}
#[cfg(test)]
-mod test {
+mod tests {
use std::{io::Cursor, path::Path};
use crate::crypto::{EncodedPassword, EncryptedFile, FileType};
}
#[cfg(test)]
-mod test;
+mod tests;
pub trait DisplayPlain {
fn display_plain(&self) -> DisplayPlainF64;
+++ /dev/null
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program. If not, see <http://www.gnu.org/licenses/>.
-
-use std::{fmt::Write, fs::File, io::BufRead, path::Path};
-
-use binrw::{io::BufReader, Endian};
-use encoding_rs::UTF_8;
-use itertools::Itertools;
-use smallstr::SmallString;
-use smallvec::SmallVec;
-
-use crate::{
- data::{ByteString, Datum, WithEncoding},
- format::{AbstractFormat, Epoch, Format, Settings, Type, UncheckedFormat, CC},
- lex::{scan::StringScanner, segment::Syntax, Punct, Token},
- settings::EndianSettings,
-};
-
-fn test(name: &str) {
- let filename = Path::new("src/format/testdata/display").join(name);
- let input = BufReader::new(File::open(&filename).unwrap());
- let settings = Settings::default()
- .with_cc(CC::A, ",,,".parse().unwrap())
- .with_cc(CC::B, "-,[[[,]]],-".parse().unwrap())
- .with_cc(CC::C, "((,[,],))".parse().unwrap())
- .with_cc(CC::D, ",XXX,,-".parse().unwrap())
- .with_cc(CC::E, ",,YYY,-".parse().unwrap());
- let endian = EndianSettings::new(Endian::Big);
- let mut value = Some(0.0);
- let mut value_name = String::new();
- for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
- let line = line.trim();
- let tokens = StringScanner::new(line, Syntax::Interactive, true)
- .unwrapped()
- .collect::<Vec<_>>();
- match &tokens[0] {
- Token::Number(number) => {
- value = if let Some(Token::Punct(Punct::Exp)) = tokens.get(1) {
- assert_eq!(tokens.len(), 3);
- let exponent = tokens[2].as_number().unwrap();
- Some(number.powf(exponent))
- } else {
- assert_eq!(tokens.len(), 1);
- Some(*number)
- };
- value_name = String::from(line);
- }
- Token::End => {
- value = None;
- value_name = String::from(line);
- }
- Token::Id(id) => {
- let format: UncheckedFormat =
- id.0.as_str()
- .parse::<AbstractFormat>()
- .unwrap()
- .try_into()
- .unwrap();
- let format: Format = format.try_into().unwrap();
- assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
- let expected = tokens[2].as_string().unwrap();
- let actual = Datum::<WithEncoding<ByteString>>::Number(value)
- .display(format)
- .with_settings(&settings)
- .with_endian(endian)
- .to_string();
- assert_eq!(
- expected,
- &actual,
- "{}:{line_number}: Error formatting {value_name} as {format}",
- filename.display()
- );
- }
- _ => panic!(),
- }
- }
-}
-
-#[test]
-fn comma() {
- test("comma.txt");
-}
-
-#[test]
-fn dot() {
- test("dot.txt");
-}
-
-#[test]
-fn dollar() {
- test("dollar.txt");
-}
-
-#[test]
-fn pct() {
- test("pct.txt");
-}
-
-#[test]
-fn e() {
- test("e.txt");
-}
-
-#[test]
-fn f() {
- test("f.txt");
-}
-
-#[test]
-fn n() {
- test("n.txt");
-}
-
-#[test]
-fn z() {
- test("z.txt");
-}
-
-#[test]
-fn cca() {
- test("cca.txt");
-}
-
-#[test]
-fn ccb() {
- test("ccb.txt");
-}
-
-#[test]
-fn ccc() {
- test("ccc.txt");
-}
-
-#[test]
-fn ccd() {
- test("ccd.txt");
-}
-
-#[test]
-fn cce() {
- test("cce.txt");
-}
-
-#[test]
-fn pibhex() {
- test("pibhex.txt");
-}
-
-#[test]
-fn rbhex() {
- test("rbhex.txt");
-}
-
-#[test]
-fn leading_zeros() {
- struct Test {
- with_leading_zero: Settings,
- without_leading_zero: Settings,
- }
-
- impl Test {
- fn new() -> Self {
- Self {
- without_leading_zero: Settings::default(),
- with_leading_zero: Settings::default().with_leading_zero(true),
- }
- }
-
- fn test_with_settings(value: f64, expected: [&str; 2], settings: &Settings) {
- let value = Datum::<WithEncoding<ByteString>>::from(value);
- for (expected, d) in expected.into_iter().zip([2, 1].into_iter()) {
- assert_eq!(
- &value
- .display(Format::new(Type::F, 5, d).unwrap())
- .with_settings(settings)
- .to_string(),
- expected
- );
- }
- }
- fn test(&self, value: f64, without: [&str; 2], with: [&str; 2]) {
- Self::test_with_settings(value, without, &self.without_leading_zero);
- Self::test_with_settings(value, with, &self.with_leading_zero);
- }
- }
- let test = Test::new();
- test.test(0.5, [" .50", " .5"], [" 0.50", " 0.5"]);
- test.test(0.99, [" .99", " 1.0"], [" 0.99", " 1.0"]);
- test.test(0.01, [" .01", " .0"], [" 0.01", " 0.0"]);
- test.test(0.0, [" .00", " .0"], [" 0.00", " 0.0"]);
- test.test(-0.0, [" .00", " .0"], [" 0.00", " 0.0"]);
- test.test(-0.5, [" -.50", " -.5"], ["-0.50", " -0.5"]);
- test.test(-0.99, [" -.99", " -1.0"], ["-0.99", " -1.0"]);
- test.test(-0.01, [" -.01", " .0"], ["-0.01", " 0.0"]);
-}
-
-#[test]
-fn non_ascii_cc() {
- fn test(settings: &Settings, value: f64, expected: &str) {
- assert_eq!(
- &Datum::<WithEncoding<ByteString>>::from(value)
- .display(Format::new(Type::CC(CC::A), 10, 2).unwrap())
- .with_settings(settings)
- .to_string(),
- expected
- );
- }
-
- let settings = Settings::default().with_cc(CC::A, "«,¥,€,»".parse().unwrap());
- test(&settings, 1.0, " ¥1.00€ ");
- test(&settings, -1.0, " «¥1.00€»");
- test(&settings, 1.5, " ¥1.50€ ");
- test(&settings, -1.5, " «¥1.50€»");
- test(&settings, 0.75, " ¥.75€ ");
- test(&settings, 1.5e10, " ¥2E+010€ ");
- test(&settings, -1.5e10, "«¥2E+010€»");
-}
-
-fn test_binhex(name: &str) {
- let filename = Path::new("src/format/testdata/display").join(name);
- let input = BufReader::new(File::open(&filename).unwrap());
- let mut value = None;
- let mut value_name = String::new();
-
- let endian = EndianSettings::new(Endian::Big);
- for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
- let line = line.trim();
- let tokens = StringScanner::new(line, Syntax::Interactive, true)
- .unwrapped()
- .collect::<Vec<_>>();
- match &tokens[0] {
- Token::Number(number) => {
- value = Some(*number);
- value_name = String::from(line);
- }
- Token::End => {
- value = None;
- value_name = String::from(line);
- }
- Token::Id(id) => {
- let format: UncheckedFormat =
- id.0.as_str()
- .parse::<AbstractFormat>()
- .unwrap()
- .try_into()
- .unwrap();
- let format: Format = format.try_into().unwrap();
- assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
- let expected = tokens[2].as_string().unwrap();
- let mut actual = SmallVec::<[u8; 16]>::new();
- Datum::<WithEncoding<ByteString>>::Number(value)
- .display(format)
- .with_endian(endian)
- .write(&mut actual, UTF_8)
- .unwrap();
- let mut actual_s = SmallString::<[u8; 32]>::new();
- for b in actual {
- write!(&mut actual_s, "{:02x}", b).unwrap();
- }
- assert_eq!(
- expected,
- &*actual_s,
- "{}:{line_number}: Error formatting {value_name} as {format}",
- filename.display()
- );
- }
- _ => panic!(),
- }
- }
-}
-
-#[test]
-fn p() {
- test_binhex("p.txt");
-}
-
-#[test]
-fn pk() {
- test_binhex("pk.txt");
-}
-
-#[test]
-fn ib() {
- test_binhex("ib.txt");
-}
-
-#[test]
-fn pib() {
- test_binhex("pib.txt");
-}
-
-#[test]
-fn rb() {
- test_binhex("rb.txt");
-}
-
-fn test_dates(format: Format, expect: &[&str]) {
- let settings = Settings::default().with_epoch(Epoch(1930));
- let parser = Type::DateTime.parser(UTF_8).with_settings(&settings);
- static INPUTS: &[&str; 20] = &[
- "10-6-1648 0:0:0",
- "30-6-1680 4:50:38.12301",
- "24-7-1716 12:31:35.23453",
- "19-6-1768 12:47:53.34505",
- "2-8-1819 1:26:0.45615",
- "27-3-1839 20:58:11.56677",
- "19-4-1903 7:36:5.18964",
- "25-8-1929 15:43:49.83132",
- "29-9-1941 4:25:9.01293",
- "19-4-1943 6:49:27.52375",
- "7-10-1943 2:57:52.01565",
- "17-3-1992 16:45:44.86529",
- "25-2-1996 21:30:57.82047",
- "29-9-41 4:25:9.15395",
- "19-4-43 6:49:27.10533",
- "7-10-43 2:57:52.48229",
- "17-3-92 16:45:44.65827",
- "25-2-96 21:30:57.58219",
- "10-11-2038 22:30:4.18347",
- "18-7-2094 1:56:51.59319",
- ];
- assert_eq!(expect.len(), INPUTS.len());
- for (input, expect) in INPUTS.iter().copied().zip_eq(expect.iter().copied()) {
- let value = parser.parse(input).unwrap().with_encoding(UTF_8);
- let formatted = value.display(format).with_settings(&settings).to_string();
- assert_eq!(&formatted, expect);
- }
-}
-
-#[test]
-fn date9() {
- test_dates(
- Format::new(Type::Date, 9, 0).unwrap(),
- &[
- "*********",
- "*********",
- "*********",
- "*********",
- "*********",
- "*********",
- "*********",
- "*********",
- "29-SEP-41",
- "19-APR-43",
- "07-OCT-43",
- "17-MAR-92",
- "25-FEB-96",
- "29-SEP-41",
- "19-APR-43",
- "07-OCT-43",
- "17-MAR-92",
- "25-FEB-96",
- "*********",
- "*********",
- ],
- );
-}
-
-#[test]
-fn date11() {
- test_dates(
- Format::new(Type::Date, 11, 0).unwrap(),
- &[
- "10-JUN-1648",
- "30-JUN-1680",
- "24-JUL-1716",
- "19-JUN-1768",
- "02-AUG-1819",
- "27-MAR-1839",
- "19-APR-1903",
- "25-AUG-1929",
- "29-SEP-1941",
- "19-APR-1943",
- "07-OCT-1943",
- "17-MAR-1992",
- "25-FEB-1996",
- "29-SEP-1941",
- "19-APR-1943",
- "07-OCT-1943",
- "17-MAR-1992",
- "25-FEB-1996",
- "10-NOV-2038",
- "18-JUL-2094",
- ],
- );
-}
-
-#[test]
-fn adate8() {
- test_dates(
- Format::new(Type::ADate, 8, 0).unwrap(),
- &[
- "********", "********", "********", "********", "********", "********", "********",
- "********", "09/29/41", "04/19/43", "10/07/43", "03/17/92", "02/25/96", "09/29/41",
- "04/19/43", "10/07/43", "03/17/92", "02/25/96", "********", "********",
- ],
- );
-}
-
-#[test]
-fn adate10() {
- test_dates(
- Format::new(Type::ADate, 10, 0).unwrap(),
- &[
- "06/10/1648",
- "06/30/1680",
- "07/24/1716",
- "06/19/1768",
- "08/02/1819",
- "03/27/1839",
- "04/19/1903",
- "08/25/1929",
- "09/29/1941",
- "04/19/1943",
- "10/07/1943",
- "03/17/1992",
- "02/25/1996",
- "09/29/1941",
- "04/19/1943",
- "10/07/1943",
- "03/17/1992",
- "02/25/1996",
- "11/10/2038",
- "07/18/2094",
- ],
- );
-}
-
-#[test]
-fn edate8() {
- test_dates(
- Format::new(Type::EDate, 8, 0).unwrap(),
- &[
- "********", "********", "********", "********", "********", "********", "********",
- "********", "29.09.41", "19.04.43", "07.10.43", "17.03.92", "25.02.96", "29.09.41",
- "19.04.43", "07.10.43", "17.03.92", "25.02.96", "********", "********",
- ],
- );
-}
-
-#[test]
-fn edate10() {
- test_dates(
- Format::new(Type::EDate, 10, 0).unwrap(),
- &[
- "10.06.1648",
- "30.06.1680",
- "24.07.1716",
- "19.06.1768",
- "02.08.1819",
- "27.03.1839",
- "19.04.1903",
- "25.08.1929",
- "29.09.1941",
- "19.04.1943",
- "07.10.1943",
- "17.03.1992",
- "25.02.1996",
- "29.09.1941",
- "19.04.1943",
- "07.10.1943",
- "17.03.1992",
- "25.02.1996",
- "10.11.2038",
- "18.07.2094",
- ],
- );
-}
-
-#[test]
-fn jdate5() {
- test_dates(
- Format::new(Type::JDate, 5, 0).unwrap(),
- &[
- "*****", "*****", "*****", "*****", "*****", "*****", "*****", "*****", "41272",
- "43109", "43280", "92077", "96056", "41272", "43109", "43280", "92077", "96056",
- "*****", "*****",
- ],
- );
-}
-
-#[test]
-fn jdate7() {
- test_dates(
- Format::new(Type::JDate, 7, 0).unwrap(),
- &[
- "1648162", "1680182", "1716206", "1768171", "1819214", "1839086", "1903109", "1929237",
- "1941272", "1943109", "1943280", "1992077", "1996056", "1941272", "1943109", "1943280",
- "1992077", "1996056", "2038314", "2094199",
- ],
- );
-}
-
-#[test]
-fn sdate8() {
- test_dates(
- Format::new(Type::SDate, 8, 0).unwrap(),
- &[
- "********", "********", "********", "********", "********", "********", "********",
- "********", "41/09/29", "43/04/19", "43/10/07", "92/03/17", "96/02/25", "41/09/29",
- "43/04/19", "43/10/07", "92/03/17", "96/02/25", "********", "********",
- ],
- );
-}
-
-#[test]
-fn sdate10() {
- test_dates(
- Format::new(Type::SDate, 10, 0).unwrap(),
- &[
- "1648/06/10",
- "1680/06/30",
- "1716/07/24",
- "1768/06/19",
- "1819/08/02",
- "1839/03/27",
- "1903/04/19",
- "1929/08/25",
- "1941/09/29",
- "1943/04/19",
- "1943/10/07",
- "1992/03/17",
- "1996/02/25",
- "1941/09/29",
- "1943/04/19",
- "1943/10/07",
- "1992/03/17",
- "1996/02/25",
- "2038/11/10",
- "2094/07/18",
- ],
- );
-}
-
-#[test]
-fn qyr6() {
- test_dates(
- Format::new(Type::QYr, 6, 0).unwrap(),
- &[
- "******", "******", "******", "******", "******", "******", "******", "******",
- "3 Q 41", "2 Q 43", "4 Q 43", "1 Q 92", "1 Q 96", "3 Q 41", "2 Q 43", "4 Q 43",
- "1 Q 92", "1 Q 96", "******", "******",
- ],
- );
-}
-
-#[test]
-fn qyr8() {
- test_dates(
- Format::new(Type::QYr, 8, 0).unwrap(),
- &[
- "2 Q 1648", "2 Q 1680", "3 Q 1716", "2 Q 1768", "3 Q 1819", "1 Q 1839", "2 Q 1903",
- "3 Q 1929", "3 Q 1941", "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "3 Q 1941",
- "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "4 Q 2038", "3 Q 2094",
- ],
- );
-}
-
-#[test]
-fn moyr6() {
- test_dates(
- Format::new(Type::MoYr, 6, 0).unwrap(),
- &[
- "******", "******", "******", "******", "******", "******", "******", "******",
- "SEP 41", "APR 43", "OCT 43", "MAR 92", "FEB 96", "SEP 41", "APR 43", "OCT 43",
- "MAR 92", "FEB 96", "******", "******",
- ],
- );
-}
-
-#[test]
-fn moyr8() {
- test_dates(
- Format::new(Type::MoYr, 8, 0).unwrap(),
- &[
- "JUN 1648", "JUN 1680", "JUL 1716", "JUN 1768", "AUG 1819", "MAR 1839", "APR 1903",
- "AUG 1929", "SEP 1941", "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "SEP 1941",
- "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "NOV 2038", "JUL 2094",
- ],
- );
-}
-
-#[test]
-fn wkyr8() {
- test_dates(
- Format::new(Type::WkYr, 8, 0).unwrap(),
- &[
- "********", "********", "********", "********", "********", "********", "********",
- "********", "39 WK 41", "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "39 WK 41",
- "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "********", "********",
- ],
- );
-}
-
-#[test]
-fn wkyr10() {
- test_dates(
- Format::new(Type::WkYr, 10, 0).unwrap(),
- &[
- "24 WK 1648",
- "26 WK 1680",
- "30 WK 1716",
- "25 WK 1768",
- "31 WK 1819",
- "13 WK 1839",
- "16 WK 1903",
- "34 WK 1929",
- "39 WK 1941",
- "16 WK 1943",
- "40 WK 1943",
- "11 WK 1992",
- " 8 WK 1996",
- "39 WK 1941",
- "16 WK 1943",
- "40 WK 1943",
- "11 WK 1992",
- " 8 WK 1996",
- "45 WK 2038",
- "29 WK 2094",
- ],
- );
-}
-
-#[test]
-fn datetime17() {
- test_dates(
- Format::new(Type::DateTime, 17, 0).unwrap(),
- &[
- "10-JUN-1648 00:00",
- "30-JUN-1680 04:50",
- "24-JUL-1716 12:31",
- "19-JUN-1768 12:47",
- "02-AUG-1819 01:26",
- "27-MAR-1839 20:58",
- "19-APR-1903 07:36",
- "25-AUG-1929 15:43",
- "29-SEP-1941 04:25",
- "19-APR-1943 06:49",
- "07-OCT-1943 02:57",
- "17-MAR-1992 16:45",
- "25-FEB-1996 21:30",
- "29-SEP-1941 04:25",
- "19-APR-1943 06:49",
- "07-OCT-1943 02:57",
- "17-MAR-1992 16:45",
- "25-FEB-1996 21:30",
- "10-NOV-2038 22:30",
- "18-JUL-2094 01:56",
- ],
- );
-}
-
-#[test]
-fn datetime18() {
- test_dates(
- Format::new(Type::DateTime, 18, 0).unwrap(),
- &[
- " 10-JUN-1648 00:00",
- " 30-JUN-1680 04:50",
- " 24-JUL-1716 12:31",
- " 19-JUN-1768 12:47",
- " 02-AUG-1819 01:26",
- " 27-MAR-1839 20:58",
- " 19-APR-1903 07:36",
- " 25-AUG-1929 15:43",
- " 29-SEP-1941 04:25",
- " 19-APR-1943 06:49",
- " 07-OCT-1943 02:57",
- " 17-MAR-1992 16:45",
- " 25-FEB-1996 21:30",
- " 29-SEP-1941 04:25",
- " 19-APR-1943 06:49",
- " 07-OCT-1943 02:57",
- " 17-MAR-1992 16:45",
- " 25-FEB-1996 21:30",
- " 10-NOV-2038 22:30",
- " 18-JUL-2094 01:56",
- ],
- );
-}
-
-#[test]
-fn datetime19() {
- test_dates(
- Format::new(Type::DateTime, 19, 0).unwrap(),
- &[
- " 10-JUN-1648 00:00",
- " 30-JUN-1680 04:50",
- " 24-JUL-1716 12:31",
- " 19-JUN-1768 12:47",
- " 02-AUG-1819 01:26",
- " 27-MAR-1839 20:58",
- " 19-APR-1903 07:36",
- " 25-AUG-1929 15:43",
- " 29-SEP-1941 04:25",
- " 19-APR-1943 06:49",
- " 07-OCT-1943 02:57",
- " 17-MAR-1992 16:45",
- " 25-FEB-1996 21:30",
- " 29-SEP-1941 04:25",
- " 19-APR-1943 06:49",
- " 07-OCT-1943 02:57",
- " 17-MAR-1992 16:45",
- " 25-FEB-1996 21:30",
- " 10-NOV-2038 22:30",
- " 18-JUL-2094 01:56",
- ],
- );
-}
-
-#[test]
-fn datetime20() {
- test_dates(
- Format::new(Type::DateTime, 20, 0).unwrap(),
- &[
- "10-JUN-1648 00:00:00",
- "30-JUN-1680 04:50:38",
- "24-JUL-1716 12:31:35",
- "19-JUN-1768 12:47:53",
- "02-AUG-1819 01:26:00",
- "27-MAR-1839 20:58:11",
- "19-APR-1903 07:36:05",
- "25-AUG-1929 15:43:49",
- "29-SEP-1941 04:25:09",
- "19-APR-1943 06:49:27",
- "07-OCT-1943 02:57:52",
- "17-MAR-1992 16:45:44",
- "25-FEB-1996 21:30:57",
- "29-SEP-1941 04:25:09",
- "19-APR-1943 06:49:27",
- "07-OCT-1943 02:57:52",
- "17-MAR-1992 16:45:44",
- "25-FEB-1996 21:30:57",
- "10-NOV-2038 22:30:04",
- "18-JUL-2094 01:56:51",
- ],
- );
-}
-
-#[test]
-fn datetime21() {
- test_dates(
- Format::new(Type::DateTime, 21, 0).unwrap(),
- &[
- " 10-JUN-1648 00:00:00",
- " 30-JUN-1680 04:50:38",
- " 24-JUL-1716 12:31:35",
- " 19-JUN-1768 12:47:53",
- " 02-AUG-1819 01:26:00",
- " 27-MAR-1839 20:58:11",
- " 19-APR-1903 07:36:05",
- " 25-AUG-1929 15:43:49",
- " 29-SEP-1941 04:25:09",
- " 19-APR-1943 06:49:27",
- " 07-OCT-1943 02:57:52",
- " 17-MAR-1992 16:45:44",
- " 25-FEB-1996 21:30:57",
- " 29-SEP-1941 04:25:09",
- " 19-APR-1943 06:49:27",
- " 07-OCT-1943 02:57:52",
- " 17-MAR-1992 16:45:44",
- " 25-FEB-1996 21:30:57",
- " 10-NOV-2038 22:30:04",
- " 18-JUL-2094 01:56:51",
- ],
- );
-}
-
-#[test]
-fn datetime22() {
- test_dates(
- Format::new(Type::DateTime, 22, 0).unwrap(),
- &[
- " 10-JUN-1648 00:00:00",
- " 30-JUN-1680 04:50:38",
- " 24-JUL-1716 12:31:35",
- " 19-JUN-1768 12:47:53",
- " 02-AUG-1819 01:26:00",
- " 27-MAR-1839 20:58:11",
- " 19-APR-1903 07:36:05",
- " 25-AUG-1929 15:43:49",
- " 29-SEP-1941 04:25:09",
- " 19-APR-1943 06:49:27",
- " 07-OCT-1943 02:57:52",
- " 17-MAR-1992 16:45:44",
- " 25-FEB-1996 21:30:57",
- " 29-SEP-1941 04:25:09",
- " 19-APR-1943 06:49:27",
- " 07-OCT-1943 02:57:52",
- " 17-MAR-1992 16:45:44",
- " 25-FEB-1996 21:30:57",
- " 10-NOV-2038 22:30:04",
- " 18-JUL-2094 01:56:51",
- ],
- );
-}
-
-#[test]
-fn datetime22_1() {
- test_dates(
- Format::new(Type::DateTime, 22, 1).unwrap(),
- &[
- "10-JUN-1648 00:00:00.0",
- "30-JUN-1680 04:50:38.1",
- "24-JUL-1716 12:31:35.2",
- "19-JUN-1768 12:47:53.3",
- "02-AUG-1819 01:26:00.5",
- "27-MAR-1839 20:58:11.6",
- "19-APR-1903 07:36:05.2",
- "25-AUG-1929 15:43:49.8",
- "29-SEP-1941 04:25:09.0",
- "19-APR-1943 06:49:27.5",
- "07-OCT-1943 02:57:52.0",
- "17-MAR-1992 16:45:44.9",
- "25-FEB-1996 21:30:57.8",
- "29-SEP-1941 04:25:09.2",
- "19-APR-1943 06:49:27.1",
- "07-OCT-1943 02:57:52.5",
- "17-MAR-1992 16:45:44.7",
- "25-FEB-1996 21:30:57.6",
- "10-NOV-2038 22:30:04.2",
- "18-JUL-2094 01:56:51.6",
- ],
- );
-}
-
-#[test]
-fn datetime23_2() {
- test_dates(
- Format::new(Type::DateTime, 23, 2).unwrap(),
- &[
- "10-JUN-1648 00:00:00.00",
- "30-JUN-1680 04:50:38.12",
- "24-JUL-1716 12:31:35.23",
- "19-JUN-1768 12:47:53.35",
- "02-AUG-1819 01:26:00.46",
- "27-MAR-1839 20:58:11.57",
- "19-APR-1903 07:36:05.19",
- "25-AUG-1929 15:43:49.83",
- "29-SEP-1941 04:25:09.01",
- "19-APR-1943 06:49:27.52",
- "07-OCT-1943 02:57:52.02",
- "17-MAR-1992 16:45:44.87",
- "25-FEB-1996 21:30:57.82",
- "29-SEP-1941 04:25:09.15",
- "19-APR-1943 06:49:27.11",
- "07-OCT-1943 02:57:52.48",
- "17-MAR-1992 16:45:44.66",
- "25-FEB-1996 21:30:57.58",
- "10-NOV-2038 22:30:04.18",
- "18-JUL-2094 01:56:51.59",
- ],
- );
-}
-
-#[test]
-fn datetime24_3() {
- test_dates(
- Format::new(Type::DateTime, 24, 3).unwrap(),
- &[
- "10-JUN-1648 00:00:00.000",
- "30-JUN-1680 04:50:38.123",
- "24-JUL-1716 12:31:35.235",
- "19-JUN-1768 12:47:53.345",
- "02-AUG-1819 01:26:00.456",
- "27-MAR-1839 20:58:11.567",
- "19-APR-1903 07:36:05.190",
- "25-AUG-1929 15:43:49.831",
- "29-SEP-1941 04:25:09.013",
- "19-APR-1943 06:49:27.524",
- "07-OCT-1943 02:57:52.016",
- "17-MAR-1992 16:45:44.865",
- "25-FEB-1996 21:30:57.820",
- "29-SEP-1941 04:25:09.154",
- "19-APR-1943 06:49:27.105",
- "07-OCT-1943 02:57:52.482",
- "17-MAR-1992 16:45:44.658",
- "25-FEB-1996 21:30:57.582",
- "10-NOV-2038 22:30:04.183",
- "18-JUL-2094 01:56:51.593",
- ],
- );
-}
-
-#[test]
-fn datetime25_4() {
- test_dates(
- Format::new(Type::DateTime, 25, 4).unwrap(),
- &[
- "10-JUN-1648 00:00:00.0000",
- "30-JUN-1680 04:50:38.1230",
- "24-JUL-1716 12:31:35.2345",
- "19-JUN-1768 12:47:53.3450",
- "02-AUG-1819 01:26:00.4562",
- "27-MAR-1839 20:58:11.5668",
- "19-APR-1903 07:36:05.1896",
- "25-AUG-1929 15:43:49.8313",
- "29-SEP-1941 04:25:09.0129",
- "19-APR-1943 06:49:27.5238",
- "07-OCT-1943 02:57:52.0156",
- "17-MAR-1992 16:45:44.8653",
- "25-FEB-1996 21:30:57.8205",
- "29-SEP-1941 04:25:09.1539",
- "19-APR-1943 06:49:27.1053",
- "07-OCT-1943 02:57:52.4823",
- "17-MAR-1992 16:45:44.6583",
- "25-FEB-1996 21:30:57.5822",
- "10-NOV-2038 22:30:04.1835",
- "18-JUL-2094 01:56:51.5932",
- ],
- );
-}
-
-#[test]
-fn datetime26_5() {
- test_dates(
- Format::new(Type::DateTime, 26, 5).unwrap(),
- &[
- "10-JUN-1648 00:00:00.00000",
- "30-JUN-1680 04:50:38.12301",
- "24-JUL-1716 12:31:35.23453",
- "19-JUN-1768 12:47:53.34505",
- "02-AUG-1819 01:26:00.45615",
- "27-MAR-1839 20:58:11.56677",
- "19-APR-1903 07:36:05.18964",
- "25-AUG-1929 15:43:49.83132",
- "29-SEP-1941 04:25:09.01293",
- "19-APR-1943 06:49:27.52375",
- "07-OCT-1943 02:57:52.01565",
- "17-MAR-1992 16:45:44.86529",
- "25-FEB-1996 21:30:57.82047",
- "29-SEP-1941 04:25:09.15395",
- "19-APR-1943 06:49:27.10533",
- "07-OCT-1943 02:57:52.48229",
- "17-MAR-1992 16:45:44.65827",
- "25-FEB-1996 21:30:57.58219",
- "10-NOV-2038 22:30:04.18347",
- "18-JUL-2094 01:56:51.59319",
- ],
- );
-}
-
-#[test]
-fn ymdhms16() {
- test_dates(
- Format::new(Type::YmdHms, 16, 0).unwrap(),
- &[
- "1648-06-10 00:00",
- "1680-06-30 04:50",
- "1716-07-24 12:31",
- "1768-06-19 12:47",
- "1819-08-02 01:26",
- "1839-03-27 20:58",
- "1903-04-19 07:36",
- "1929-08-25 15:43",
- "1941-09-29 04:25",
- "1943-04-19 06:49",
- "1943-10-07 02:57",
- "1992-03-17 16:45",
- "1996-02-25 21:30",
- "1941-09-29 04:25",
- "1943-04-19 06:49",
- "1943-10-07 02:57",
- "1992-03-17 16:45",
- "1996-02-25 21:30",
- "2038-11-10 22:30",
- "2094-07-18 01:56",
- ],
- );
-}
-
-#[test]
-fn ymdhms17() {
- test_dates(
- Format::new(Type::YmdHms, 17, 0).unwrap(),
- &[
- " 1648-06-10 00:00",
- " 1680-06-30 04:50",
- " 1716-07-24 12:31",
- " 1768-06-19 12:47",
- " 1819-08-02 01:26",
- " 1839-03-27 20:58",
- " 1903-04-19 07:36",
- " 1929-08-25 15:43",
- " 1941-09-29 04:25",
- " 1943-04-19 06:49",
- " 1943-10-07 02:57",
- " 1992-03-17 16:45",
- " 1996-02-25 21:30",
- " 1941-09-29 04:25",
- " 1943-04-19 06:49",
- " 1943-10-07 02:57",
- " 1992-03-17 16:45",
- " 1996-02-25 21:30",
- " 2038-11-10 22:30",
- " 2094-07-18 01:56",
- ],
- );
-}
-
-#[test]
-fn ymdhms18() {
- test_dates(
- Format::new(Type::YmdHms, 18, 0).unwrap(),
- &[
- " 1648-06-10 00:00",
- " 1680-06-30 04:50",
- " 1716-07-24 12:31",
- " 1768-06-19 12:47",
- " 1819-08-02 01:26",
- " 1839-03-27 20:58",
- " 1903-04-19 07:36",
- " 1929-08-25 15:43",
- " 1941-09-29 04:25",
- " 1943-04-19 06:49",
- " 1943-10-07 02:57",
- " 1992-03-17 16:45",
- " 1996-02-25 21:30",
- " 1941-09-29 04:25",
- " 1943-04-19 06:49",
- " 1943-10-07 02:57",
- " 1992-03-17 16:45",
- " 1996-02-25 21:30",
- " 2038-11-10 22:30",
- " 2094-07-18 01:56",
- ],
- );
-}
-
-#[test]
-fn ymdhms19() {
- test_dates(
- Format::new(Type::YmdHms, 19, 0).unwrap(),
- &[
- "1648-06-10 00:00:00",
- "1680-06-30 04:50:38",
- "1716-07-24 12:31:35",
- "1768-06-19 12:47:53",
- "1819-08-02 01:26:00",
- "1839-03-27 20:58:11",
- "1903-04-19 07:36:05",
- "1929-08-25 15:43:49",
- "1941-09-29 04:25:09",
- "1943-04-19 06:49:27",
- "1943-10-07 02:57:52",
- "1992-03-17 16:45:44",
- "1996-02-25 21:30:57",
- "1941-09-29 04:25:09",
- "1943-04-19 06:49:27",
- "1943-10-07 02:57:52",
- "1992-03-17 16:45:44",
- "1996-02-25 21:30:57",
- "2038-11-10 22:30:04",
- "2094-07-18 01:56:51",
- ],
- );
-}
-
-#[test]
-fn ymdhms20() {
- test_dates(
- Format::new(Type::YmdHms, 20, 0).unwrap(),
- &[
- " 1648-06-10 00:00:00",
- " 1680-06-30 04:50:38",
- " 1716-07-24 12:31:35",
- " 1768-06-19 12:47:53",
- " 1819-08-02 01:26:00",
- " 1839-03-27 20:58:11",
- " 1903-04-19 07:36:05",
- " 1929-08-25 15:43:49",
- " 1941-09-29 04:25:09",
- " 1943-04-19 06:49:27",
- " 1943-10-07 02:57:52",
- " 1992-03-17 16:45:44",
- " 1996-02-25 21:30:57",
- " 1941-09-29 04:25:09",
- " 1943-04-19 06:49:27",
- " 1943-10-07 02:57:52",
- " 1992-03-17 16:45:44",
- " 1996-02-25 21:30:57",
- " 2038-11-10 22:30:04",
- " 2094-07-18 01:56:51",
- ],
- );
-}
-
-#[test]
-fn ymdhms21() {
- test_dates(
- Format::new(Type::YmdHms, 21, 0).unwrap(),
- &[
- " 1648-06-10 00:00:00",
- " 1680-06-30 04:50:38",
- " 1716-07-24 12:31:35",
- " 1768-06-19 12:47:53",
- " 1819-08-02 01:26:00",
- " 1839-03-27 20:58:11",
- " 1903-04-19 07:36:05",
- " 1929-08-25 15:43:49",
- " 1941-09-29 04:25:09",
- " 1943-04-19 06:49:27",
- " 1943-10-07 02:57:52",
- " 1992-03-17 16:45:44",
- " 1996-02-25 21:30:57",
- " 1941-09-29 04:25:09",
- " 1943-04-19 06:49:27",
- " 1943-10-07 02:57:52",
- " 1992-03-17 16:45:44",
- " 1996-02-25 21:30:57",
- " 2038-11-10 22:30:04",
- " 2094-07-18 01:56:51",
- ],
- );
-}
-
-#[test]
-fn ymdhms21_1() {
- test_dates(
- Format::new(Type::YmdHms, 21, 1).unwrap(),
- &[
- "1648-06-10 00:00:00.0",
- "1680-06-30 04:50:38.1",
- "1716-07-24 12:31:35.2",
- "1768-06-19 12:47:53.3",
- "1819-08-02 01:26:00.5",
- "1839-03-27 20:58:11.6",
- "1903-04-19 07:36:05.2",
- "1929-08-25 15:43:49.8",
- "1941-09-29 04:25:09.0",
- "1943-04-19 06:49:27.5",
- "1943-10-07 02:57:52.0",
- "1992-03-17 16:45:44.9",
- "1996-02-25 21:30:57.8",
- "1941-09-29 04:25:09.2",
- "1943-04-19 06:49:27.1",
- "1943-10-07 02:57:52.5",
- "1992-03-17 16:45:44.7",
- "1996-02-25 21:30:57.6",
- "2038-11-10 22:30:04.2",
- "2094-07-18 01:56:51.6",
- ],
- );
-}
-
-#[test]
-fn ymdhms22_2() {
- test_dates(
- Format::new(Type::YmdHms, 22, 2).unwrap(),
- &[
- "1648-06-10 00:00:00.00",
- "1680-06-30 04:50:38.12",
- "1716-07-24 12:31:35.23",
- "1768-06-19 12:47:53.35",
- "1819-08-02 01:26:00.46",
- "1839-03-27 20:58:11.57",
- "1903-04-19 07:36:05.19",
- "1929-08-25 15:43:49.83",
- "1941-09-29 04:25:09.01",
- "1943-04-19 06:49:27.52",
- "1943-10-07 02:57:52.02",
- "1992-03-17 16:45:44.87",
- "1996-02-25 21:30:57.82",
- "1941-09-29 04:25:09.15",
- "1943-04-19 06:49:27.11",
- "1943-10-07 02:57:52.48",
- "1992-03-17 16:45:44.66",
- "1996-02-25 21:30:57.58",
- "2038-11-10 22:30:04.18",
- "2094-07-18 01:56:51.59",
- ],
- );
-}
-
-#[test]
-fn ymdhms23_3() {
- test_dates(
- Format::new(Type::YmdHms, 23, 3).unwrap(),
- &[
- "1648-06-10 00:00:00.000",
- "1680-06-30 04:50:38.123",
- "1716-07-24 12:31:35.235",
- "1768-06-19 12:47:53.345",
- "1819-08-02 01:26:00.456",
- "1839-03-27 20:58:11.567",
- "1903-04-19 07:36:05.190",
- "1929-08-25 15:43:49.831",
- "1941-09-29 04:25:09.013",
- "1943-04-19 06:49:27.524",
- "1943-10-07 02:57:52.016",
- "1992-03-17 16:45:44.865",
- "1996-02-25 21:30:57.820",
- "1941-09-29 04:25:09.154",
- "1943-04-19 06:49:27.105",
- "1943-10-07 02:57:52.482",
- "1992-03-17 16:45:44.658",
- "1996-02-25 21:30:57.582",
- "2038-11-10 22:30:04.183",
- "2094-07-18 01:56:51.593",
- ],
- );
-}
-
-#[test]
-fn ymdhms24_4() {
- test_dates(
- Format::new(Type::YmdHms, 24, 4).unwrap(),
- &[
- "1648-06-10 00:00:00.0000",
- "1680-06-30 04:50:38.1230",
- "1716-07-24 12:31:35.2345",
- "1768-06-19 12:47:53.3450",
- "1819-08-02 01:26:00.4562",
- "1839-03-27 20:58:11.5668",
- "1903-04-19 07:36:05.1896",
- "1929-08-25 15:43:49.8313",
- "1941-09-29 04:25:09.0129",
- "1943-04-19 06:49:27.5238",
- "1943-10-07 02:57:52.0156",
- "1992-03-17 16:45:44.8653",
- "1996-02-25 21:30:57.8205",
- "1941-09-29 04:25:09.1539",
- "1943-04-19 06:49:27.1053",
- "1943-10-07 02:57:52.4823",
- "1992-03-17 16:45:44.6583",
- "1996-02-25 21:30:57.5822",
- "2038-11-10 22:30:04.1835",
- "2094-07-18 01:56:51.5932",
- ],
- );
-}
-
-#[test]
-fn ymdhms25_5() {
- test_dates(
- Format::new(Type::YmdHms, 25, 5).unwrap(),
- &[
- "1648-06-10 00:00:00.00000",
- "1680-06-30 04:50:38.12301",
- "1716-07-24 12:31:35.23453",
- "1768-06-19 12:47:53.34505",
- "1819-08-02 01:26:00.45615",
- "1839-03-27 20:58:11.56677",
- "1903-04-19 07:36:05.18964",
- "1929-08-25 15:43:49.83132",
- "1941-09-29 04:25:09.01293",
- "1943-04-19 06:49:27.52375",
- "1943-10-07 02:57:52.01565",
- "1992-03-17 16:45:44.86529",
- "1996-02-25 21:30:57.82047",
- "1941-09-29 04:25:09.15395",
- "1943-04-19 06:49:27.10533",
- "1943-10-07 02:57:52.48229",
- "1992-03-17 16:45:44.65827",
- "1996-02-25 21:30:57.58219",
- "2038-11-10 22:30:04.18347",
- "2094-07-18 01:56:51.59319",
- ],
- );
-}
-
-fn test_times(format: Format, name: &str) {
- let directory = Path::new("src/format/testdata/display");
- let input_filename = directory.join("time-input.txt");
- let input = BufReader::new(File::open(&input_filename).unwrap());
-
- let output_filename = directory.join(name);
- let output = BufReader::new(File::open(&output_filename).unwrap());
-
- let parser = Type::DTime.parser(UTF_8);
- for ((input, expect), line_number) in input
- .lines()
- .map(|r| r.unwrap())
- .zip_eq(output.lines().map(|r| r.unwrap()))
- .zip(1..)
- {
- let formatted = parser
- .parse(input)
- .unwrap()
- .with_encoding(UTF_8)
- .display(format)
- .to_string();
- assert!(
- formatted == expect,
- "formatting {}:{line_number} as {format}:\n actual: {formatted:?}\nexpected: {expect:?}",
- input_filename.display()
- );
- }
-}
-
-#[test]
-fn time5() {
- test_times(Format::new(Type::Time, 5, 0).unwrap(), "time5.txt");
-}
-
-#[test]
-fn time6() {
- test_times(Format::new(Type::Time, 6, 0).unwrap(), "time6.txt");
-}
-
-#[test]
-fn time7() {
- test_times(Format::new(Type::Time, 7, 0).unwrap(), "time7.txt");
-}
-
-#[test]
-fn time8() {
- test_times(Format::new(Type::Time, 8, 0).unwrap(), "time8.txt");
-}
-
-#[test]
-fn time9() {
- test_times(Format::new(Type::Time, 9, 0).unwrap(), "time9.txt");
-}
-
-#[test]
-fn time10() {
- test_times(Format::new(Type::Time, 10, 0).unwrap(), "time10.txt");
-}
-
-#[test]
-fn time10_1() {
- test_times(Format::new(Type::Time, 10, 1).unwrap(), "time10.1.txt");
-}
-
-#[test]
-fn time11() {
- test_times(Format::new(Type::Time, 11, 0).unwrap(), "time11.txt");
-}
-
-#[test]
-fn time11_1() {
- test_times(Format::new(Type::Time, 11, 1).unwrap(), "time11.1.txt");
-}
-
-#[test]
-fn time11_2() {
- test_times(Format::new(Type::Time, 11, 2).unwrap(), "time11.2.txt");
-}
-
-#[test]
-fn time12() {
- test_times(Format::new(Type::Time, 12, 0).unwrap(), "time12.txt");
-}
-
-#[test]
-fn time12_1() {
- test_times(Format::new(Type::Time, 12, 1).unwrap(), "time12.1.txt");
-}
-
-#[test]
-fn time12_2() {
- test_times(Format::new(Type::Time, 12, 2).unwrap(), "time12.2.txt");
-}
-
-#[test]
-fn time12_3() {
- test_times(Format::new(Type::Time, 12, 3).unwrap(), "time12.3.txt");
-}
-
-#[test]
-fn time13() {
- test_times(Format::new(Type::Time, 13, 0).unwrap(), "time13.txt");
-}
-
-#[test]
-fn time13_1() {
- test_times(Format::new(Type::Time, 13, 1).unwrap(), "time13.1.txt");
-}
-
-#[test]
-fn time13_2() {
- test_times(Format::new(Type::Time, 13, 2).unwrap(), "time13.2.txt");
-}
-
-#[test]
-fn time13_3() {
- test_times(Format::new(Type::Time, 13, 3).unwrap(), "time13.3.txt");
-}
-
-#[test]
-fn time13_4() {
- test_times(Format::new(Type::Time, 13, 4).unwrap(), "time13.4.txt");
-}
-
-#[test]
-fn time14() {
- test_times(Format::new(Type::Time, 14, 0).unwrap(), "time14.txt");
-}
-
-#[test]
-fn time14_1() {
- test_times(Format::new(Type::Time, 14, 1).unwrap(), "time14.1.txt");
-}
-
-#[test]
-fn time14_2() {
- test_times(Format::new(Type::Time, 14, 2).unwrap(), "time14.2.txt");
-}
-
-#[test]
-fn time14_3() {
- test_times(Format::new(Type::Time, 14, 3).unwrap(), "time14.3.txt");
-}
-
-#[test]
-fn time14_4() {
- test_times(Format::new(Type::Time, 14, 4).unwrap(), "time14.4.txt");
-}
-
-#[test]
-fn time14_5() {
- test_times(Format::new(Type::Time, 14, 5).unwrap(), "time14.5.txt");
-}
-
-#[test]
-fn time15() {
- test_times(Format::new(Type::Time, 15, 0).unwrap(), "time15.txt");
-}
-
-#[test]
-fn time15_1() {
- test_times(Format::new(Type::Time, 15, 1).unwrap(), "time15.1.txt");
-}
-
-#[test]
-fn time15_2() {
- test_times(Format::new(Type::Time, 15, 2).unwrap(), "time15.2.txt");
-}
-
-#[test]
-fn time15_3() {
- test_times(Format::new(Type::Time, 15, 3).unwrap(), "time15.3.txt");
-}
-
-#[test]
-fn time15_4() {
- test_times(Format::new(Type::Time, 15, 4).unwrap(), "time15.4.txt");
-}
-
-#[test]
-fn time15_5() {
- test_times(Format::new(Type::Time, 15, 5).unwrap(), "time15.5.txt");
-}
-
-#[test]
-fn time15_6() {
- test_times(Format::new(Type::Time, 15, 6).unwrap(), "time15.6.txt");
-}
-
-#[test]
-fn mtime5() {
- test_times(Format::new(Type::MTime, 5, 0).unwrap(), "mtime5.txt");
-}
-
-#[test]
-fn mtime6() {
- test_times(Format::new(Type::MTime, 6, 0).unwrap(), "mtime6.txt");
-}
-
-#[test]
-fn mtime7() {
- test_times(Format::new(Type::MTime, 7, 0).unwrap(), "mtime7.txt");
-}
-
-#[test]
-fn mtime7_1() {
- test_times(Format::new(Type::MTime, 7, 1).unwrap(), "mtime7.1.txt");
-}
-
-#[test]
-fn mtime8() {
- test_times(Format::new(Type::MTime, 8, 0).unwrap(), "mtime8.txt");
-}
-
-#[test]
-fn mtime8_1() {
- test_times(Format::new(Type::MTime, 8, 1).unwrap(), "mtime8.1.txt");
-}
-
-#[test]
-fn mtime8_2() {
- test_times(Format::new(Type::MTime, 8, 2).unwrap(), "mtime8.2.txt");
-}
-
-#[test]
-fn mtime9() {
- test_times(Format::new(Type::MTime, 9, 0).unwrap(), "mtime9.txt");
-}
-
-#[test]
-fn mtime9_1() {
- test_times(Format::new(Type::MTime, 9, 1).unwrap(), "mtime9.1.txt");
-}
-
-#[test]
-fn mtime9_2() {
- test_times(Format::new(Type::MTime, 9, 2).unwrap(), "mtime9.2.txt");
-}
-
-#[test]
-fn mtime9_3() {
- test_times(Format::new(Type::MTime, 9, 3).unwrap(), "mtime9.3.txt");
-}
-
-#[test]
-fn mtime10() {
- test_times(Format::new(Type::MTime, 10, 0).unwrap(), "mtime10.txt");
-}
-
-#[test]
-fn mtime10_1() {
- test_times(Format::new(Type::MTime, 10, 1).unwrap(), "mtime10.1.txt");
-}
-
-#[test]
-fn mtime10_2() {
- test_times(Format::new(Type::MTime, 10, 2).unwrap(), "mtime10.2.txt");
-}
-
-#[test]
-fn mtime10_3() {
- test_times(Format::new(Type::MTime, 10, 3).unwrap(), "mtime10.3.txt");
-}
-
-#[test]
-fn mtime10_4() {
- test_times(Format::new(Type::MTime, 10, 4).unwrap(), "mtime10.4.txt");
-}
-
-#[test]
-fn mtime11() {
- test_times(Format::new(Type::MTime, 11, 0).unwrap(), "mtime11.txt");
-}
-
-#[test]
-fn mtime11_1() {
- test_times(Format::new(Type::MTime, 11, 1).unwrap(), "mtime11.1.txt");
-}
-
-#[test]
-fn mtime11_2() {
- test_times(Format::new(Type::MTime, 11, 2).unwrap(), "mtime11.2.txt");
-}
-
-#[test]
-fn mtime11_3() {
- test_times(Format::new(Type::MTime, 11, 3).unwrap(), "mtime11.3.txt");
-}
-
-#[test]
-fn mtime11_4() {
- test_times(Format::new(Type::MTime, 11, 4).unwrap(), "mtime11.4.txt");
-}
-
-#[test]
-fn mtime11_5() {
- test_times(Format::new(Type::MTime, 11, 5).unwrap(), "mtime11.5.txt");
-}
-
-#[test]
-fn mtime12_5() {
- test_times(Format::new(Type::MTime, 12, 5).unwrap(), "mtime12.5.txt");
-}
-
-#[test]
-fn mtime13_5() {
- test_times(Format::new(Type::MTime, 13, 5).unwrap(), "mtime13.5.txt");
-}
-
-#[test]
-fn mtime14_5() {
- test_times(Format::new(Type::MTime, 14, 5).unwrap(), "mtime14.5.txt");
-}
-
-#[test]
-fn mtime15_5() {
- test_times(Format::new(Type::MTime, 15, 5).unwrap(), "mtime15.5.txt");
-}
-
-#[test]
-fn mtime16_5() {
- test_times(Format::new(Type::MTime, 16, 5).unwrap(), "mtime16.5.txt");
-}
-
-#[test]
-fn dtime8() {
- test_times(Format::new(Type::DTime, 8, 0).unwrap(), "dtime8.txt");
-}
-
-#[test]
-fn dtime9() {
- test_times(Format::new(Type::DTime, 9, 0).unwrap(), "dtime9.txt");
-}
-
-#[test]
-fn dtime10() {
- test_times(Format::new(Type::DTime, 10, 0).unwrap(), "dtime10.txt");
-}
-
-#[test]
-fn dtime11() {
- test_times(Format::new(Type::DTime, 11, 0).unwrap(), "dtime11.txt");
-}
-
-#[test]
-fn dtime12() {
- test_times(Format::new(Type::DTime, 12, 0).unwrap(), "dtime12.txt");
-}
-
-#[test]
-fn dtime13() {
- test_times(Format::new(Type::DTime, 13, 0).unwrap(), "dtime13.txt");
-}
-
-#[test]
-fn dtime13_1() {
- test_times(Format::new(Type::DTime, 13, 1).unwrap(), "dtime13.1.txt");
-}
-
-#[test]
-fn dtime14() {
- test_times(Format::new(Type::DTime, 14, 0).unwrap(), "dtime14.txt");
-}
-
-#[test]
-fn dtime14_1() {
- test_times(Format::new(Type::DTime, 14, 1).unwrap(), "dtime14.1.txt");
-}
-
-#[test]
-fn dtime14_2() {
- test_times(Format::new(Type::DTime, 14, 2).unwrap(), "dtime14.2.txt");
-}
-
-#[test]
-fn dtime15() {
- test_times(Format::new(Type::DTime, 15, 0).unwrap(), "dtime15.txt");
-}
-
-#[test]
-fn dtime15_1() {
- test_times(Format::new(Type::DTime, 15, 1).unwrap(), "dtime15.1.txt");
-}
-
-#[test]
-fn dtime15_2() {
- test_times(Format::new(Type::DTime, 15, 2).unwrap(), "dtime15.2.txt");
-}
-
-#[test]
-fn dtime15_3() {
- test_times(Format::new(Type::DTime, 15, 3).unwrap(), "dtime15.3.txt");
-}
-
-#[test]
-fn dtime16() {
- test_times(Format::new(Type::DTime, 16, 0).unwrap(), "dtime16.txt");
-}
-
-#[test]
-fn dtime16_1() {
- test_times(Format::new(Type::DTime, 16, 1).unwrap(), "dtime16.1.txt");
-}
-
-#[test]
-fn dtime16_2() {
- test_times(Format::new(Type::DTime, 16, 2).unwrap(), "dtime16.2.txt");
-}
-
-#[test]
-fn dtime16_3() {
- test_times(Format::new(Type::DTime, 16, 3).unwrap(), "dtime16.3.txt");
-}
-
-#[test]
-fn dtime16_4() {
- test_times(Format::new(Type::DTime, 16, 4).unwrap(), "dtime16.4.txt");
-}
-
-#[test]
-fn dtime17() {
- test_times(Format::new(Type::DTime, 17, 0).unwrap(), "dtime17.txt");
-}
-
-#[test]
-fn dtime17_1() {
- test_times(Format::new(Type::DTime, 17, 1).unwrap(), "dtime17.1.txt");
-}
-
-#[test]
-fn dtime17_2() {
- test_times(Format::new(Type::DTime, 17, 2).unwrap(), "dtime17.2.txt");
-}
-
-#[test]
-fn dtime17_3() {
- test_times(Format::new(Type::DTime, 17, 3).unwrap(), "dtime17.3.txt");
-}
-
-#[test]
-fn dtime17_4() {
- test_times(Format::new(Type::DTime, 17, 4).unwrap(), "dtime17.4.txt");
-}
-
-#[test]
-fn dtime17_5() {
- test_times(Format::new(Type::DTime, 17, 5).unwrap(), "dtime17.5.txt");
-}
-
-#[test]
-fn dtime18() {
- test_times(Format::new(Type::DTime, 18, 0).unwrap(), "dtime18.txt");
-}
-
-#[test]
-fn dtime18_1() {
- test_times(Format::new(Type::DTime, 18, 1).unwrap(), "dtime18.1.txt");
-}
-
-#[test]
-fn dtime18_2() {
- test_times(Format::new(Type::DTime, 18, 2).unwrap(), "dtime18.2.txt");
-}
-
-#[test]
-fn dtime18_3() {
- test_times(Format::new(Type::DTime, 18, 3).unwrap(), "dtime18.3.txt");
-}
-
-#[test]
-fn dtime18_4() {
- test_times(Format::new(Type::DTime, 18, 4).unwrap(), "dtime18.4.txt");
-}
-
-#[test]
-fn dtime18_5() {
- test_times(Format::new(Type::DTime, 18, 5).unwrap(), "dtime18.5.txt");
-}
-
-#[test]
-fn dtime18_6() {
- test_times(Format::new(Type::DTime, 18, 6).unwrap(), "dtime18.6.txt");
-}
--- /dev/null
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see <http://www.gnu.org/licenses/>.
+
+use std::{fmt::Write, fs::File, io::BufRead, path::Path};
+
+use binrw::{io::BufReader, Endian};
+use encoding_rs::UTF_8;
+use itertools::Itertools;
+use smallstr::SmallString;
+use smallvec::SmallVec;
+
+use crate::{
+ data::{ByteString, Datum, WithEncoding},
+ format::{AbstractFormat, Epoch, Format, Settings, Type, UncheckedFormat, CC},
+ lex::{scan::StringScanner, segment::Syntax, Punct, Token},
+ settings::EndianSettings,
+};
+
+fn test(name: &str) {
+ let filename = Path::new("src/format/testdata/display").join(name);
+ let input = BufReader::new(File::open(&filename).unwrap());
+ let settings = Settings::default()
+ .with_cc(CC::A, ",,,".parse().unwrap())
+ .with_cc(CC::B, "-,[[[,]]],-".parse().unwrap())
+ .with_cc(CC::C, "((,[,],))".parse().unwrap())
+ .with_cc(CC::D, ",XXX,,-".parse().unwrap())
+ .with_cc(CC::E, ",,YYY,-".parse().unwrap());
+ let endian = EndianSettings::new(Endian::Big);
+ let mut value = Some(0.0);
+ let mut value_name = String::new();
+ for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
+ let line = line.trim();
+ let tokens = StringScanner::new(line, Syntax::Interactive, true)
+ .unwrapped()
+ .collect::<Vec<_>>();
+ match &tokens[0] {
+ Token::Number(number) => {
+ value = if let Some(Token::Punct(Punct::Exp)) = tokens.get(1) {
+ assert_eq!(tokens.len(), 3);
+ let exponent = tokens[2].as_number().unwrap();
+ Some(number.powf(exponent))
+ } else {
+ assert_eq!(tokens.len(), 1);
+ Some(*number)
+ };
+ value_name = String::from(line);
+ }
+ Token::End => {
+ value = None;
+ value_name = String::from(line);
+ }
+ Token::Id(id) => {
+ let format: UncheckedFormat =
+ id.0.as_str()
+ .parse::<AbstractFormat>()
+ .unwrap()
+ .try_into()
+ .unwrap();
+ let format: Format = format.try_into().unwrap();
+ assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
+ let expected = tokens[2].as_string().unwrap();
+ let actual = Datum::<WithEncoding<ByteString>>::Number(value)
+ .display(format)
+ .with_settings(&settings)
+ .with_endian(endian)
+ .to_string();
+ assert_eq!(
+ expected,
+ &actual,
+ "{}:{line_number}: Error formatting {value_name} as {format}",
+ filename.display()
+ );
+ }
+ _ => panic!(),
+ }
+ }
+}
+
+#[test]
+fn comma() {
+ test("comma.txt");
+}
+
+#[test]
+fn dot() {
+ test("dot.txt");
+}
+
+#[test]
+fn dollar() {
+ test("dollar.txt");
+}
+
+#[test]
+fn pct() {
+ test("pct.txt");
+}
+
+#[test]
+fn e() {
+ test("e.txt");
+}
+
+#[test]
+fn f() {
+ test("f.txt");
+}
+
+#[test]
+fn n() {
+ test("n.txt");
+}
+
+#[test]
+fn z() {
+ test("z.txt");
+}
+
+#[test]
+fn cca() {
+ test("cca.txt");
+}
+
+#[test]
+fn ccb() {
+ test("ccb.txt");
+}
+
+#[test]
+fn ccc() {
+ test("ccc.txt");
+}
+
+#[test]
+fn ccd() {
+ test("ccd.txt");
+}
+
+#[test]
+fn cce() {
+ test("cce.txt");
+}
+
+#[test]
+fn pibhex() {
+ test("pibhex.txt");
+}
+
+#[test]
+fn rbhex() {
+ test("rbhex.txt");
+}
+
+#[test]
+fn leading_zeros() {
+ struct Test {
+ with_leading_zero: Settings,
+ without_leading_zero: Settings,
+ }
+
+ impl Test {
+ fn new() -> Self {
+ Self {
+ without_leading_zero: Settings::default(),
+ with_leading_zero: Settings::default().with_leading_zero(true),
+ }
+ }
+
+ fn test_with_settings(value: f64, expected: [&str; 2], settings: &Settings) {
+ let value = Datum::<WithEncoding<ByteString>>::from(value);
+ for (expected, d) in expected.into_iter().zip([2, 1].into_iter()) {
+ assert_eq!(
+ &value
+ .display(Format::new(Type::F, 5, d).unwrap())
+ .with_settings(settings)
+ .to_string(),
+ expected
+ );
+ }
+ }
+ fn test(&self, value: f64, without: [&str; 2], with: [&str; 2]) {
+ Self::test_with_settings(value, without, &self.without_leading_zero);
+ Self::test_with_settings(value, with, &self.with_leading_zero);
+ }
+ }
+ let test = Test::new();
+ test.test(0.5, [" .50", " .5"], [" 0.50", " 0.5"]);
+ test.test(0.99, [" .99", " 1.0"], [" 0.99", " 1.0"]);
+ test.test(0.01, [" .01", " .0"], [" 0.01", " 0.0"]);
+ test.test(0.0, [" .00", " .0"], [" 0.00", " 0.0"]);
+ test.test(-0.0, [" .00", " .0"], [" 0.00", " 0.0"]);
+ test.test(-0.5, [" -.50", " -.5"], ["-0.50", " -0.5"]);
+ test.test(-0.99, [" -.99", " -1.0"], ["-0.99", " -1.0"]);
+ test.test(-0.01, [" -.01", " .0"], ["-0.01", " 0.0"]);
+}
+
+#[test]
+fn non_ascii_cc() {
+ fn test(settings: &Settings, value: f64, expected: &str) {
+ assert_eq!(
+ &Datum::<WithEncoding<ByteString>>::from(value)
+ .display(Format::new(Type::CC(CC::A), 10, 2).unwrap())
+ .with_settings(settings)
+ .to_string(),
+ expected
+ );
+ }
+
+ let settings = Settings::default().with_cc(CC::A, "«,¥,€,»".parse().unwrap());
+ test(&settings, 1.0, " ¥1.00€ ");
+ test(&settings, -1.0, " «¥1.00€»");
+ test(&settings, 1.5, " ¥1.50€ ");
+ test(&settings, -1.5, " «¥1.50€»");
+ test(&settings, 0.75, " ¥.75€ ");
+ test(&settings, 1.5e10, " ¥2E+010€ ");
+ test(&settings, -1.5e10, "«¥2E+010€»");
+}
+
+fn test_binhex(name: &str) {
+ let filename = Path::new("src/format/testdata/display").join(name);
+ let input = BufReader::new(File::open(&filename).unwrap());
+ let mut value = None;
+ let mut value_name = String::new();
+
+ let endian = EndianSettings::new(Endian::Big);
+ for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
+ let line = line.trim();
+ let tokens = StringScanner::new(line, Syntax::Interactive, true)
+ .unwrapped()
+ .collect::<Vec<_>>();
+ match &tokens[0] {
+ Token::Number(number) => {
+ value = Some(*number);
+ value_name = String::from(line);
+ }
+ Token::End => {
+ value = None;
+ value_name = String::from(line);
+ }
+ Token::Id(id) => {
+ let format: UncheckedFormat =
+ id.0.as_str()
+ .parse::<AbstractFormat>()
+ .unwrap()
+ .try_into()
+ .unwrap();
+ let format: Format = format.try_into().unwrap();
+ assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
+ let expected = tokens[2].as_string().unwrap();
+ let mut actual = SmallVec::<[u8; 16]>::new();
+ Datum::<WithEncoding<ByteString>>::Number(value)
+ .display(format)
+ .with_endian(endian)
+ .write(&mut actual, UTF_8)
+ .unwrap();
+ let mut actual_s = SmallString::<[u8; 32]>::new();
+ for b in actual {
+ write!(&mut actual_s, "{:02x}", b).unwrap();
+ }
+ assert_eq!(
+ expected,
+ &*actual_s,
+ "{}:{line_number}: Error formatting {value_name} as {format}",
+ filename.display()
+ );
+ }
+ _ => panic!(),
+ }
+ }
+}
+
+#[test]
+fn p() {
+ test_binhex("p.txt");
+}
+
+#[test]
+fn pk() {
+ test_binhex("pk.txt");
+}
+
+#[test]
+fn ib() {
+ test_binhex("ib.txt");
+}
+
+#[test]
+fn pib() {
+ test_binhex("pib.txt");
+}
+
+#[test]
+fn rb() {
+ test_binhex("rb.txt");
+}
+
+fn test_dates(format: Format, expect: &[&str]) {
+ let settings = Settings::default().with_epoch(Epoch(1930));
+ let parser = Type::DateTime.parser(UTF_8).with_settings(&settings);
+ static INPUTS: &[&str; 20] = &[
+ "10-6-1648 0:0:0",
+ "30-6-1680 4:50:38.12301",
+ "24-7-1716 12:31:35.23453",
+ "19-6-1768 12:47:53.34505",
+ "2-8-1819 1:26:0.45615",
+ "27-3-1839 20:58:11.56677",
+ "19-4-1903 7:36:5.18964",
+ "25-8-1929 15:43:49.83132",
+ "29-9-1941 4:25:9.01293",
+ "19-4-1943 6:49:27.52375",
+ "7-10-1943 2:57:52.01565",
+ "17-3-1992 16:45:44.86529",
+ "25-2-1996 21:30:57.82047",
+ "29-9-41 4:25:9.15395",
+ "19-4-43 6:49:27.10533",
+ "7-10-43 2:57:52.48229",
+ "17-3-92 16:45:44.65827",
+ "25-2-96 21:30:57.58219",
+ "10-11-2038 22:30:4.18347",
+ "18-7-2094 1:56:51.59319",
+ ];
+ assert_eq!(expect.len(), INPUTS.len());
+ for (input, expect) in INPUTS.iter().copied().zip_eq(expect.iter().copied()) {
+ let value = parser.parse(input).unwrap().with_encoding(UTF_8);
+ let formatted = value.display(format).with_settings(&settings).to_string();
+ assert_eq!(&formatted, expect);
+ }
+}
+
+#[test]
+fn date9() {
+ test_dates(
+ Format::new(Type::Date, 9, 0).unwrap(),
+ &[
+ "*********",
+ "*********",
+ "*********",
+ "*********",
+ "*********",
+ "*********",
+ "*********",
+ "*********",
+ "29-SEP-41",
+ "19-APR-43",
+ "07-OCT-43",
+ "17-MAR-92",
+ "25-FEB-96",
+ "29-SEP-41",
+ "19-APR-43",
+ "07-OCT-43",
+ "17-MAR-92",
+ "25-FEB-96",
+ "*********",
+ "*********",
+ ],
+ );
+}
+
+#[test]
+fn date11() {
+ test_dates(
+ Format::new(Type::Date, 11, 0).unwrap(),
+ &[
+ "10-JUN-1648",
+ "30-JUN-1680",
+ "24-JUL-1716",
+ "19-JUN-1768",
+ "02-AUG-1819",
+ "27-MAR-1839",
+ "19-APR-1903",
+ "25-AUG-1929",
+ "29-SEP-1941",
+ "19-APR-1943",
+ "07-OCT-1943",
+ "17-MAR-1992",
+ "25-FEB-1996",
+ "29-SEP-1941",
+ "19-APR-1943",
+ "07-OCT-1943",
+ "17-MAR-1992",
+ "25-FEB-1996",
+ "10-NOV-2038",
+ "18-JUL-2094",
+ ],
+ );
+}
+
+#[test]
+fn adate8() {
+ test_dates(
+ Format::new(Type::ADate, 8, 0).unwrap(),
+ &[
+ "********", "********", "********", "********", "********", "********", "********",
+ "********", "09/29/41", "04/19/43", "10/07/43", "03/17/92", "02/25/96", "09/29/41",
+ "04/19/43", "10/07/43", "03/17/92", "02/25/96", "********", "********",
+ ],
+ );
+}
+
+#[test]
+fn adate10() {
+ test_dates(
+ Format::new(Type::ADate, 10, 0).unwrap(),
+ &[
+ "06/10/1648",
+ "06/30/1680",
+ "07/24/1716",
+ "06/19/1768",
+ "08/02/1819",
+ "03/27/1839",
+ "04/19/1903",
+ "08/25/1929",
+ "09/29/1941",
+ "04/19/1943",
+ "10/07/1943",
+ "03/17/1992",
+ "02/25/1996",
+ "09/29/1941",
+ "04/19/1943",
+ "10/07/1943",
+ "03/17/1992",
+ "02/25/1996",
+ "11/10/2038",
+ "07/18/2094",
+ ],
+ );
+}
+
+#[test]
+fn edate8() {
+ test_dates(
+ Format::new(Type::EDate, 8, 0).unwrap(),
+ &[
+ "********", "********", "********", "********", "********", "********", "********",
+ "********", "29.09.41", "19.04.43", "07.10.43", "17.03.92", "25.02.96", "29.09.41",
+ "19.04.43", "07.10.43", "17.03.92", "25.02.96", "********", "********",
+ ],
+ );
+}
+
+#[test]
+fn edate10() {
+ test_dates(
+ Format::new(Type::EDate, 10, 0).unwrap(),
+ &[
+ "10.06.1648",
+ "30.06.1680",
+ "24.07.1716",
+ "19.06.1768",
+ "02.08.1819",
+ "27.03.1839",
+ "19.04.1903",
+ "25.08.1929",
+ "29.09.1941",
+ "19.04.1943",
+ "07.10.1943",
+ "17.03.1992",
+ "25.02.1996",
+ "29.09.1941",
+ "19.04.1943",
+ "07.10.1943",
+ "17.03.1992",
+ "25.02.1996",
+ "10.11.2038",
+ "18.07.2094",
+ ],
+ );
+}
+
+#[test]
+fn jdate5() {
+ test_dates(
+ Format::new(Type::JDate, 5, 0).unwrap(),
+ &[
+ "*****", "*****", "*****", "*****", "*****", "*****", "*****", "*****", "41272",
+ "43109", "43280", "92077", "96056", "41272", "43109", "43280", "92077", "96056",
+ "*****", "*****",
+ ],
+ );
+}
+
+#[test]
+fn jdate7() {
+ test_dates(
+ Format::new(Type::JDate, 7, 0).unwrap(),
+ &[
+ "1648162", "1680182", "1716206", "1768171", "1819214", "1839086", "1903109", "1929237",
+ "1941272", "1943109", "1943280", "1992077", "1996056", "1941272", "1943109", "1943280",
+ "1992077", "1996056", "2038314", "2094199",
+ ],
+ );
+}
+
+#[test]
+fn sdate8() {
+ test_dates(
+ Format::new(Type::SDate, 8, 0).unwrap(),
+ &[
+ "********", "********", "********", "********", "********", "********", "********",
+ "********", "41/09/29", "43/04/19", "43/10/07", "92/03/17", "96/02/25", "41/09/29",
+ "43/04/19", "43/10/07", "92/03/17", "96/02/25", "********", "********",
+ ],
+ );
+}
+
+#[test]
+fn sdate10() {
+ test_dates(
+ Format::new(Type::SDate, 10, 0).unwrap(),
+ &[
+ "1648/06/10",
+ "1680/06/30",
+ "1716/07/24",
+ "1768/06/19",
+ "1819/08/02",
+ "1839/03/27",
+ "1903/04/19",
+ "1929/08/25",
+ "1941/09/29",
+ "1943/04/19",
+ "1943/10/07",
+ "1992/03/17",
+ "1996/02/25",
+ "1941/09/29",
+ "1943/04/19",
+ "1943/10/07",
+ "1992/03/17",
+ "1996/02/25",
+ "2038/11/10",
+ "2094/07/18",
+ ],
+ );
+}
+
+#[test]
+fn qyr6() {
+ test_dates(
+ Format::new(Type::QYr, 6, 0).unwrap(),
+ &[
+ "******", "******", "******", "******", "******", "******", "******", "******",
+ "3 Q 41", "2 Q 43", "4 Q 43", "1 Q 92", "1 Q 96", "3 Q 41", "2 Q 43", "4 Q 43",
+ "1 Q 92", "1 Q 96", "******", "******",
+ ],
+ );
+}
+
+#[test]
+fn qyr8() {
+ test_dates(
+ Format::new(Type::QYr, 8, 0).unwrap(),
+ &[
+ "2 Q 1648", "2 Q 1680", "3 Q 1716", "2 Q 1768", "3 Q 1819", "1 Q 1839", "2 Q 1903",
+ "3 Q 1929", "3 Q 1941", "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "3 Q 1941",
+ "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "4 Q 2038", "3 Q 2094",
+ ],
+ );
+}
+
+#[test]
+fn moyr6() {
+ test_dates(
+ Format::new(Type::MoYr, 6, 0).unwrap(),
+ &[
+ "******", "******", "******", "******", "******", "******", "******", "******",
+ "SEP 41", "APR 43", "OCT 43", "MAR 92", "FEB 96", "SEP 41", "APR 43", "OCT 43",
+ "MAR 92", "FEB 96", "******", "******",
+ ],
+ );
+}
+
+#[test]
+fn moyr8() {
+ test_dates(
+ Format::new(Type::MoYr, 8, 0).unwrap(),
+ &[
+ "JUN 1648", "JUN 1680", "JUL 1716", "JUN 1768", "AUG 1819", "MAR 1839", "APR 1903",
+ "AUG 1929", "SEP 1941", "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "SEP 1941",
+ "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "NOV 2038", "JUL 2094",
+ ],
+ );
+}
+
+#[test]
+fn wkyr8() {
+ test_dates(
+ Format::new(Type::WkYr, 8, 0).unwrap(),
+ &[
+ "********", "********", "********", "********", "********", "********", "********",
+ "********", "39 WK 41", "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "39 WK 41",
+ "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "********", "********",
+ ],
+ );
+}
+
+#[test]
+fn wkyr10() {
+ test_dates(
+ Format::new(Type::WkYr, 10, 0).unwrap(),
+ &[
+ "24 WK 1648",
+ "26 WK 1680",
+ "30 WK 1716",
+ "25 WK 1768",
+ "31 WK 1819",
+ "13 WK 1839",
+ "16 WK 1903",
+ "34 WK 1929",
+ "39 WK 1941",
+ "16 WK 1943",
+ "40 WK 1943",
+ "11 WK 1992",
+ " 8 WK 1996",
+ "39 WK 1941",
+ "16 WK 1943",
+ "40 WK 1943",
+ "11 WK 1992",
+ " 8 WK 1996",
+ "45 WK 2038",
+ "29 WK 2094",
+ ],
+ );
+}
+
+#[test]
+fn datetime17() {
+ test_dates(
+ Format::new(Type::DateTime, 17, 0).unwrap(),
+ &[
+ "10-JUN-1648 00:00",
+ "30-JUN-1680 04:50",
+ "24-JUL-1716 12:31",
+ "19-JUN-1768 12:47",
+ "02-AUG-1819 01:26",
+ "27-MAR-1839 20:58",
+ "19-APR-1903 07:36",
+ "25-AUG-1929 15:43",
+ "29-SEP-1941 04:25",
+ "19-APR-1943 06:49",
+ "07-OCT-1943 02:57",
+ "17-MAR-1992 16:45",
+ "25-FEB-1996 21:30",
+ "29-SEP-1941 04:25",
+ "19-APR-1943 06:49",
+ "07-OCT-1943 02:57",
+ "17-MAR-1992 16:45",
+ "25-FEB-1996 21:30",
+ "10-NOV-2038 22:30",
+ "18-JUL-2094 01:56",
+ ],
+ );
+}
+
+#[test]
+fn datetime18() {
+ test_dates(
+ Format::new(Type::DateTime, 18, 0).unwrap(),
+ &[
+ " 10-JUN-1648 00:00",
+ " 30-JUN-1680 04:50",
+ " 24-JUL-1716 12:31",
+ " 19-JUN-1768 12:47",
+ " 02-AUG-1819 01:26",
+ " 27-MAR-1839 20:58",
+ " 19-APR-1903 07:36",
+ " 25-AUG-1929 15:43",
+ " 29-SEP-1941 04:25",
+ " 19-APR-1943 06:49",
+ " 07-OCT-1943 02:57",
+ " 17-MAR-1992 16:45",
+ " 25-FEB-1996 21:30",
+ " 29-SEP-1941 04:25",
+ " 19-APR-1943 06:49",
+ " 07-OCT-1943 02:57",
+ " 17-MAR-1992 16:45",
+ " 25-FEB-1996 21:30",
+ " 10-NOV-2038 22:30",
+ " 18-JUL-2094 01:56",
+ ],
+ );
+}
+
+#[test]
+fn datetime19() {
+ test_dates(
+ Format::new(Type::DateTime, 19, 0).unwrap(),
+ &[
+ " 10-JUN-1648 00:00",
+ " 30-JUN-1680 04:50",
+ " 24-JUL-1716 12:31",
+ " 19-JUN-1768 12:47",
+ " 02-AUG-1819 01:26",
+ " 27-MAR-1839 20:58",
+ " 19-APR-1903 07:36",
+ " 25-AUG-1929 15:43",
+ " 29-SEP-1941 04:25",
+ " 19-APR-1943 06:49",
+ " 07-OCT-1943 02:57",
+ " 17-MAR-1992 16:45",
+ " 25-FEB-1996 21:30",
+ " 29-SEP-1941 04:25",
+ " 19-APR-1943 06:49",
+ " 07-OCT-1943 02:57",
+ " 17-MAR-1992 16:45",
+ " 25-FEB-1996 21:30",
+ " 10-NOV-2038 22:30",
+ " 18-JUL-2094 01:56",
+ ],
+ );
+}
+
+#[test]
+fn datetime20() {
+ test_dates(
+ Format::new(Type::DateTime, 20, 0).unwrap(),
+ &[
+ "10-JUN-1648 00:00:00",
+ "30-JUN-1680 04:50:38",
+ "24-JUL-1716 12:31:35",
+ "19-JUN-1768 12:47:53",
+ "02-AUG-1819 01:26:00",
+ "27-MAR-1839 20:58:11",
+ "19-APR-1903 07:36:05",
+ "25-AUG-1929 15:43:49",
+ "29-SEP-1941 04:25:09",
+ "19-APR-1943 06:49:27",
+ "07-OCT-1943 02:57:52",
+ "17-MAR-1992 16:45:44",
+ "25-FEB-1996 21:30:57",
+ "29-SEP-1941 04:25:09",
+ "19-APR-1943 06:49:27",
+ "07-OCT-1943 02:57:52",
+ "17-MAR-1992 16:45:44",
+ "25-FEB-1996 21:30:57",
+ "10-NOV-2038 22:30:04",
+ "18-JUL-2094 01:56:51",
+ ],
+ );
+}
+
+#[test]
+fn datetime21() {
+ test_dates(
+ Format::new(Type::DateTime, 21, 0).unwrap(),
+ &[
+ " 10-JUN-1648 00:00:00",
+ " 30-JUN-1680 04:50:38",
+ " 24-JUL-1716 12:31:35",
+ " 19-JUN-1768 12:47:53",
+ " 02-AUG-1819 01:26:00",
+ " 27-MAR-1839 20:58:11",
+ " 19-APR-1903 07:36:05",
+ " 25-AUG-1929 15:43:49",
+ " 29-SEP-1941 04:25:09",
+ " 19-APR-1943 06:49:27",
+ " 07-OCT-1943 02:57:52",
+ " 17-MAR-1992 16:45:44",
+ " 25-FEB-1996 21:30:57",
+ " 29-SEP-1941 04:25:09",
+ " 19-APR-1943 06:49:27",
+ " 07-OCT-1943 02:57:52",
+ " 17-MAR-1992 16:45:44",
+ " 25-FEB-1996 21:30:57",
+ " 10-NOV-2038 22:30:04",
+ " 18-JUL-2094 01:56:51",
+ ],
+ );
+}
+
+#[test]
+fn datetime22() {
+ test_dates(
+ Format::new(Type::DateTime, 22, 0).unwrap(),
+ &[
+ " 10-JUN-1648 00:00:00",
+ " 30-JUN-1680 04:50:38",
+ " 24-JUL-1716 12:31:35",
+ " 19-JUN-1768 12:47:53",
+ " 02-AUG-1819 01:26:00",
+ " 27-MAR-1839 20:58:11",
+ " 19-APR-1903 07:36:05",
+ " 25-AUG-1929 15:43:49",
+ " 29-SEP-1941 04:25:09",
+ " 19-APR-1943 06:49:27",
+ " 07-OCT-1943 02:57:52",
+ " 17-MAR-1992 16:45:44",
+ " 25-FEB-1996 21:30:57",
+ " 29-SEP-1941 04:25:09",
+ " 19-APR-1943 06:49:27",
+ " 07-OCT-1943 02:57:52",
+ " 17-MAR-1992 16:45:44",
+ " 25-FEB-1996 21:30:57",
+ " 10-NOV-2038 22:30:04",
+ " 18-JUL-2094 01:56:51",
+ ],
+ );
+}
+
+#[test]
+fn datetime22_1() {
+ test_dates(
+ Format::new(Type::DateTime, 22, 1).unwrap(),
+ &[
+ "10-JUN-1648 00:00:00.0",
+ "30-JUN-1680 04:50:38.1",
+ "24-JUL-1716 12:31:35.2",
+ "19-JUN-1768 12:47:53.3",
+ "02-AUG-1819 01:26:00.5",
+ "27-MAR-1839 20:58:11.6",
+ "19-APR-1903 07:36:05.2",
+ "25-AUG-1929 15:43:49.8",
+ "29-SEP-1941 04:25:09.0",
+ "19-APR-1943 06:49:27.5",
+ "07-OCT-1943 02:57:52.0",
+ "17-MAR-1992 16:45:44.9",
+ "25-FEB-1996 21:30:57.8",
+ "29-SEP-1941 04:25:09.2",
+ "19-APR-1943 06:49:27.1",
+ "07-OCT-1943 02:57:52.5",
+ "17-MAR-1992 16:45:44.7",
+ "25-FEB-1996 21:30:57.6",
+ "10-NOV-2038 22:30:04.2",
+ "18-JUL-2094 01:56:51.6",
+ ],
+ );
+}
+
+#[test]
+fn datetime23_2() {
+ test_dates(
+ Format::new(Type::DateTime, 23, 2).unwrap(),
+ &[
+ "10-JUN-1648 00:00:00.00",
+ "30-JUN-1680 04:50:38.12",
+ "24-JUL-1716 12:31:35.23",
+ "19-JUN-1768 12:47:53.35",
+ "02-AUG-1819 01:26:00.46",
+ "27-MAR-1839 20:58:11.57",
+ "19-APR-1903 07:36:05.19",
+ "25-AUG-1929 15:43:49.83",
+ "29-SEP-1941 04:25:09.01",
+ "19-APR-1943 06:49:27.52",
+ "07-OCT-1943 02:57:52.02",
+ "17-MAR-1992 16:45:44.87",
+ "25-FEB-1996 21:30:57.82",
+ "29-SEP-1941 04:25:09.15",
+ "19-APR-1943 06:49:27.11",
+ "07-OCT-1943 02:57:52.48",
+ "17-MAR-1992 16:45:44.66",
+ "25-FEB-1996 21:30:57.58",
+ "10-NOV-2038 22:30:04.18",
+ "18-JUL-2094 01:56:51.59",
+ ],
+ );
+}
+
+#[test]
+fn datetime24_3() {
+ test_dates(
+ Format::new(Type::DateTime, 24, 3).unwrap(),
+ &[
+ "10-JUN-1648 00:00:00.000",
+ "30-JUN-1680 04:50:38.123",
+ "24-JUL-1716 12:31:35.235",
+ "19-JUN-1768 12:47:53.345",
+ "02-AUG-1819 01:26:00.456",
+ "27-MAR-1839 20:58:11.567",
+ "19-APR-1903 07:36:05.190",
+ "25-AUG-1929 15:43:49.831",
+ "29-SEP-1941 04:25:09.013",
+ "19-APR-1943 06:49:27.524",
+ "07-OCT-1943 02:57:52.016",
+ "17-MAR-1992 16:45:44.865",
+ "25-FEB-1996 21:30:57.820",
+ "29-SEP-1941 04:25:09.154",
+ "19-APR-1943 06:49:27.105",
+ "07-OCT-1943 02:57:52.482",
+ "17-MAR-1992 16:45:44.658",
+ "25-FEB-1996 21:30:57.582",
+ "10-NOV-2038 22:30:04.183",
+ "18-JUL-2094 01:56:51.593",
+ ],
+ );
+}
+
+#[test]
+fn datetime25_4() {
+ test_dates(
+ Format::new(Type::DateTime, 25, 4).unwrap(),
+ &[
+ "10-JUN-1648 00:00:00.0000",
+ "30-JUN-1680 04:50:38.1230",
+ "24-JUL-1716 12:31:35.2345",
+ "19-JUN-1768 12:47:53.3450",
+ "02-AUG-1819 01:26:00.4562",
+ "27-MAR-1839 20:58:11.5668",
+ "19-APR-1903 07:36:05.1896",
+ "25-AUG-1929 15:43:49.8313",
+ "29-SEP-1941 04:25:09.0129",
+ "19-APR-1943 06:49:27.5238",
+ "07-OCT-1943 02:57:52.0156",
+ "17-MAR-1992 16:45:44.8653",
+ "25-FEB-1996 21:30:57.8205",
+ "29-SEP-1941 04:25:09.1539",
+ "19-APR-1943 06:49:27.1053",
+ "07-OCT-1943 02:57:52.4823",
+ "17-MAR-1992 16:45:44.6583",
+ "25-FEB-1996 21:30:57.5822",
+ "10-NOV-2038 22:30:04.1835",
+ "18-JUL-2094 01:56:51.5932",
+ ],
+ );
+}
+
+#[test]
+fn datetime26_5() {
+ test_dates(
+ Format::new(Type::DateTime, 26, 5).unwrap(),
+ &[
+ "10-JUN-1648 00:00:00.00000",
+ "30-JUN-1680 04:50:38.12301",
+ "24-JUL-1716 12:31:35.23453",
+ "19-JUN-1768 12:47:53.34505",
+ "02-AUG-1819 01:26:00.45615",
+ "27-MAR-1839 20:58:11.56677",
+ "19-APR-1903 07:36:05.18964",
+ "25-AUG-1929 15:43:49.83132",
+ "29-SEP-1941 04:25:09.01293",
+ "19-APR-1943 06:49:27.52375",
+ "07-OCT-1943 02:57:52.01565",
+ "17-MAR-1992 16:45:44.86529",
+ "25-FEB-1996 21:30:57.82047",
+ "29-SEP-1941 04:25:09.15395",
+ "19-APR-1943 06:49:27.10533",
+ "07-OCT-1943 02:57:52.48229",
+ "17-MAR-1992 16:45:44.65827",
+ "25-FEB-1996 21:30:57.58219",
+ "10-NOV-2038 22:30:04.18347",
+ "18-JUL-2094 01:56:51.59319",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms16() {
+ test_dates(
+ Format::new(Type::YmdHms, 16, 0).unwrap(),
+ &[
+ "1648-06-10 00:00",
+ "1680-06-30 04:50",
+ "1716-07-24 12:31",
+ "1768-06-19 12:47",
+ "1819-08-02 01:26",
+ "1839-03-27 20:58",
+ "1903-04-19 07:36",
+ "1929-08-25 15:43",
+ "1941-09-29 04:25",
+ "1943-04-19 06:49",
+ "1943-10-07 02:57",
+ "1992-03-17 16:45",
+ "1996-02-25 21:30",
+ "1941-09-29 04:25",
+ "1943-04-19 06:49",
+ "1943-10-07 02:57",
+ "1992-03-17 16:45",
+ "1996-02-25 21:30",
+ "2038-11-10 22:30",
+ "2094-07-18 01:56",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms17() {
+ test_dates(
+ Format::new(Type::YmdHms, 17, 0).unwrap(),
+ &[
+ " 1648-06-10 00:00",
+ " 1680-06-30 04:50",
+ " 1716-07-24 12:31",
+ " 1768-06-19 12:47",
+ " 1819-08-02 01:26",
+ " 1839-03-27 20:58",
+ " 1903-04-19 07:36",
+ " 1929-08-25 15:43",
+ " 1941-09-29 04:25",
+ " 1943-04-19 06:49",
+ " 1943-10-07 02:57",
+ " 1992-03-17 16:45",
+ " 1996-02-25 21:30",
+ " 1941-09-29 04:25",
+ " 1943-04-19 06:49",
+ " 1943-10-07 02:57",
+ " 1992-03-17 16:45",
+ " 1996-02-25 21:30",
+ " 2038-11-10 22:30",
+ " 2094-07-18 01:56",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms18() {
+ test_dates(
+ Format::new(Type::YmdHms, 18, 0).unwrap(),
+ &[
+ " 1648-06-10 00:00",
+ " 1680-06-30 04:50",
+ " 1716-07-24 12:31",
+ " 1768-06-19 12:47",
+ " 1819-08-02 01:26",
+ " 1839-03-27 20:58",
+ " 1903-04-19 07:36",
+ " 1929-08-25 15:43",
+ " 1941-09-29 04:25",
+ " 1943-04-19 06:49",
+ " 1943-10-07 02:57",
+ " 1992-03-17 16:45",
+ " 1996-02-25 21:30",
+ " 1941-09-29 04:25",
+ " 1943-04-19 06:49",
+ " 1943-10-07 02:57",
+ " 1992-03-17 16:45",
+ " 1996-02-25 21:30",
+ " 2038-11-10 22:30",
+ " 2094-07-18 01:56",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms19() {
+ test_dates(
+ Format::new(Type::YmdHms, 19, 0).unwrap(),
+ &[
+ "1648-06-10 00:00:00",
+ "1680-06-30 04:50:38",
+ "1716-07-24 12:31:35",
+ "1768-06-19 12:47:53",
+ "1819-08-02 01:26:00",
+ "1839-03-27 20:58:11",
+ "1903-04-19 07:36:05",
+ "1929-08-25 15:43:49",
+ "1941-09-29 04:25:09",
+ "1943-04-19 06:49:27",
+ "1943-10-07 02:57:52",
+ "1992-03-17 16:45:44",
+ "1996-02-25 21:30:57",
+ "1941-09-29 04:25:09",
+ "1943-04-19 06:49:27",
+ "1943-10-07 02:57:52",
+ "1992-03-17 16:45:44",
+ "1996-02-25 21:30:57",
+ "2038-11-10 22:30:04",
+ "2094-07-18 01:56:51",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms20() {
+ test_dates(
+ Format::new(Type::YmdHms, 20, 0).unwrap(),
+ &[
+ " 1648-06-10 00:00:00",
+ " 1680-06-30 04:50:38",
+ " 1716-07-24 12:31:35",
+ " 1768-06-19 12:47:53",
+ " 1819-08-02 01:26:00",
+ " 1839-03-27 20:58:11",
+ " 1903-04-19 07:36:05",
+ " 1929-08-25 15:43:49",
+ " 1941-09-29 04:25:09",
+ " 1943-04-19 06:49:27",
+ " 1943-10-07 02:57:52",
+ " 1992-03-17 16:45:44",
+ " 1996-02-25 21:30:57",
+ " 1941-09-29 04:25:09",
+ " 1943-04-19 06:49:27",
+ " 1943-10-07 02:57:52",
+ " 1992-03-17 16:45:44",
+ " 1996-02-25 21:30:57",
+ " 2038-11-10 22:30:04",
+ " 2094-07-18 01:56:51",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms21() {
+ test_dates(
+ Format::new(Type::YmdHms, 21, 0).unwrap(),
+ &[
+ " 1648-06-10 00:00:00",
+ " 1680-06-30 04:50:38",
+ " 1716-07-24 12:31:35",
+ " 1768-06-19 12:47:53",
+ " 1819-08-02 01:26:00",
+ " 1839-03-27 20:58:11",
+ " 1903-04-19 07:36:05",
+ " 1929-08-25 15:43:49",
+ " 1941-09-29 04:25:09",
+ " 1943-04-19 06:49:27",
+ " 1943-10-07 02:57:52",
+ " 1992-03-17 16:45:44",
+ " 1996-02-25 21:30:57",
+ " 1941-09-29 04:25:09",
+ " 1943-04-19 06:49:27",
+ " 1943-10-07 02:57:52",
+ " 1992-03-17 16:45:44",
+ " 1996-02-25 21:30:57",
+ " 2038-11-10 22:30:04",
+ " 2094-07-18 01:56:51",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms21_1() {
+ test_dates(
+ Format::new(Type::YmdHms, 21, 1).unwrap(),
+ &[
+ "1648-06-10 00:00:00.0",
+ "1680-06-30 04:50:38.1",
+ "1716-07-24 12:31:35.2",
+ "1768-06-19 12:47:53.3",
+ "1819-08-02 01:26:00.5",
+ "1839-03-27 20:58:11.6",
+ "1903-04-19 07:36:05.2",
+ "1929-08-25 15:43:49.8",
+ "1941-09-29 04:25:09.0",
+ "1943-04-19 06:49:27.5",
+ "1943-10-07 02:57:52.0",
+ "1992-03-17 16:45:44.9",
+ "1996-02-25 21:30:57.8",
+ "1941-09-29 04:25:09.2",
+ "1943-04-19 06:49:27.1",
+ "1943-10-07 02:57:52.5",
+ "1992-03-17 16:45:44.7",
+ "1996-02-25 21:30:57.6",
+ "2038-11-10 22:30:04.2",
+ "2094-07-18 01:56:51.6",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms22_2() {
+ test_dates(
+ Format::new(Type::YmdHms, 22, 2).unwrap(),
+ &[
+ "1648-06-10 00:00:00.00",
+ "1680-06-30 04:50:38.12",
+ "1716-07-24 12:31:35.23",
+ "1768-06-19 12:47:53.35",
+ "1819-08-02 01:26:00.46",
+ "1839-03-27 20:58:11.57",
+ "1903-04-19 07:36:05.19",
+ "1929-08-25 15:43:49.83",
+ "1941-09-29 04:25:09.01",
+ "1943-04-19 06:49:27.52",
+ "1943-10-07 02:57:52.02",
+ "1992-03-17 16:45:44.87",
+ "1996-02-25 21:30:57.82",
+ "1941-09-29 04:25:09.15",
+ "1943-04-19 06:49:27.11",
+ "1943-10-07 02:57:52.48",
+ "1992-03-17 16:45:44.66",
+ "1996-02-25 21:30:57.58",
+ "2038-11-10 22:30:04.18",
+ "2094-07-18 01:56:51.59",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms23_3() {
+ test_dates(
+ Format::new(Type::YmdHms, 23, 3).unwrap(),
+ &[
+ "1648-06-10 00:00:00.000",
+ "1680-06-30 04:50:38.123",
+ "1716-07-24 12:31:35.235",
+ "1768-06-19 12:47:53.345",
+ "1819-08-02 01:26:00.456",
+ "1839-03-27 20:58:11.567",
+ "1903-04-19 07:36:05.190",
+ "1929-08-25 15:43:49.831",
+ "1941-09-29 04:25:09.013",
+ "1943-04-19 06:49:27.524",
+ "1943-10-07 02:57:52.016",
+ "1992-03-17 16:45:44.865",
+ "1996-02-25 21:30:57.820",
+ "1941-09-29 04:25:09.154",
+ "1943-04-19 06:49:27.105",
+ "1943-10-07 02:57:52.482",
+ "1992-03-17 16:45:44.658",
+ "1996-02-25 21:30:57.582",
+ "2038-11-10 22:30:04.183",
+ "2094-07-18 01:56:51.593",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms24_4() {
+ test_dates(
+ Format::new(Type::YmdHms, 24, 4).unwrap(),
+ &[
+ "1648-06-10 00:00:00.0000",
+ "1680-06-30 04:50:38.1230",
+ "1716-07-24 12:31:35.2345",
+ "1768-06-19 12:47:53.3450",
+ "1819-08-02 01:26:00.4562",
+ "1839-03-27 20:58:11.5668",
+ "1903-04-19 07:36:05.1896",
+ "1929-08-25 15:43:49.8313",
+ "1941-09-29 04:25:09.0129",
+ "1943-04-19 06:49:27.5238",
+ "1943-10-07 02:57:52.0156",
+ "1992-03-17 16:45:44.8653",
+ "1996-02-25 21:30:57.8205",
+ "1941-09-29 04:25:09.1539",
+ "1943-04-19 06:49:27.1053",
+ "1943-10-07 02:57:52.4823",
+ "1992-03-17 16:45:44.6583",
+ "1996-02-25 21:30:57.5822",
+ "2038-11-10 22:30:04.1835",
+ "2094-07-18 01:56:51.5932",
+ ],
+ );
+}
+
+#[test]
+fn ymdhms25_5() {
+ test_dates(
+ Format::new(Type::YmdHms, 25, 5).unwrap(),
+ &[
+ "1648-06-10 00:00:00.00000",
+ "1680-06-30 04:50:38.12301",
+ "1716-07-24 12:31:35.23453",
+ "1768-06-19 12:47:53.34505",
+ "1819-08-02 01:26:00.45615",
+ "1839-03-27 20:58:11.56677",
+ "1903-04-19 07:36:05.18964",
+ "1929-08-25 15:43:49.83132",
+ "1941-09-29 04:25:09.01293",
+ "1943-04-19 06:49:27.52375",
+ "1943-10-07 02:57:52.01565",
+ "1992-03-17 16:45:44.86529",
+ "1996-02-25 21:30:57.82047",
+ "1941-09-29 04:25:09.15395",
+ "1943-04-19 06:49:27.10533",
+ "1943-10-07 02:57:52.48229",
+ "1992-03-17 16:45:44.65827",
+ "1996-02-25 21:30:57.58219",
+ "2038-11-10 22:30:04.18347",
+ "2094-07-18 01:56:51.59319",
+ ],
+ );
+}
+
+fn test_times(format: Format, name: &str) {
+ let directory = Path::new("src/format/testdata/display");
+ let input_filename = directory.join("time-input.txt");
+ let input = BufReader::new(File::open(&input_filename).unwrap());
+
+ let output_filename = directory.join(name);
+ let output = BufReader::new(File::open(&output_filename).unwrap());
+
+ let parser = Type::DTime.parser(UTF_8);
+ for ((input, expect), line_number) in input
+ .lines()
+ .map(|r| r.unwrap())
+ .zip_eq(output.lines().map(|r| r.unwrap()))
+ .zip(1..)
+ {
+ let formatted = parser
+ .parse(input)
+ .unwrap()
+ .with_encoding(UTF_8)
+ .display(format)
+ .to_string();
+ assert!(
+ formatted == expect,
+ "formatting {}:{line_number} as {format}:\n actual: {formatted:?}\nexpected: {expect:?}",
+ input_filename.display()
+ );
+ }
+}
+
+#[test]
+fn time5() {
+ test_times(Format::new(Type::Time, 5, 0).unwrap(), "time5.txt");
+}
+
+#[test]
+fn time6() {
+ test_times(Format::new(Type::Time, 6, 0).unwrap(), "time6.txt");
+}
+
+#[test]
+fn time7() {
+ test_times(Format::new(Type::Time, 7, 0).unwrap(), "time7.txt");
+}
+
+#[test]
+fn time8() {
+ test_times(Format::new(Type::Time, 8, 0).unwrap(), "time8.txt");
+}
+
+#[test]
+fn time9() {
+ test_times(Format::new(Type::Time, 9, 0).unwrap(), "time9.txt");
+}
+
+#[test]
+fn time10() {
+ test_times(Format::new(Type::Time, 10, 0).unwrap(), "time10.txt");
+}
+
+#[test]
+fn time10_1() {
+ test_times(Format::new(Type::Time, 10, 1).unwrap(), "time10.1.txt");
+}
+
+#[test]
+fn time11() {
+ test_times(Format::new(Type::Time, 11, 0).unwrap(), "time11.txt");
+}
+
+#[test]
+fn time11_1() {
+ test_times(Format::new(Type::Time, 11, 1).unwrap(), "time11.1.txt");
+}
+
+#[test]
+fn time11_2() {
+ test_times(Format::new(Type::Time, 11, 2).unwrap(), "time11.2.txt");
+}
+
+#[test]
+fn time12() {
+ test_times(Format::new(Type::Time, 12, 0).unwrap(), "time12.txt");
+}
+
+#[test]
+fn time12_1() {
+ test_times(Format::new(Type::Time, 12, 1).unwrap(), "time12.1.txt");
+}
+
+#[test]
+fn time12_2() {
+ test_times(Format::new(Type::Time, 12, 2).unwrap(), "time12.2.txt");
+}
+
+#[test]
+fn time12_3() {
+ test_times(Format::new(Type::Time, 12, 3).unwrap(), "time12.3.txt");
+}
+
+#[test]
+fn time13() {
+ test_times(Format::new(Type::Time, 13, 0).unwrap(), "time13.txt");
+}
+
+#[test]
+fn time13_1() {
+ test_times(Format::new(Type::Time, 13, 1).unwrap(), "time13.1.txt");
+}
+
+#[test]
+fn time13_2() {
+ test_times(Format::new(Type::Time, 13, 2).unwrap(), "time13.2.txt");
+}
+
+#[test]
+fn time13_3() {
+ test_times(Format::new(Type::Time, 13, 3).unwrap(), "time13.3.txt");
+}
+
+#[test]
+fn time13_4() {
+ test_times(Format::new(Type::Time, 13, 4).unwrap(), "time13.4.txt");
+}
+
+#[test]
+fn time14() {
+ test_times(Format::new(Type::Time, 14, 0).unwrap(), "time14.txt");
+}
+
+#[test]
+fn time14_1() {
+ test_times(Format::new(Type::Time, 14, 1).unwrap(), "time14.1.txt");
+}
+
+#[test]
+fn time14_2() {
+ test_times(Format::new(Type::Time, 14, 2).unwrap(), "time14.2.txt");
+}
+
+#[test]
+fn time14_3() {
+ test_times(Format::new(Type::Time, 14, 3).unwrap(), "time14.3.txt");
+}
+
+#[test]
+fn time14_4() {
+ test_times(Format::new(Type::Time, 14, 4).unwrap(), "time14.4.txt");
+}
+
+#[test]
+fn time14_5() {
+ test_times(Format::new(Type::Time, 14, 5).unwrap(), "time14.5.txt");
+}
+
+#[test]
+fn time15() {
+ test_times(Format::new(Type::Time, 15, 0).unwrap(), "time15.txt");
+}
+
+#[test]
+fn time15_1() {
+ test_times(Format::new(Type::Time, 15, 1).unwrap(), "time15.1.txt");
+}
+
+#[test]
+fn time15_2() {
+ test_times(Format::new(Type::Time, 15, 2).unwrap(), "time15.2.txt");
+}
+
+#[test]
+fn time15_3() {
+ test_times(Format::new(Type::Time, 15, 3).unwrap(), "time15.3.txt");
+}
+
+#[test]
+fn time15_4() {
+ test_times(Format::new(Type::Time, 15, 4).unwrap(), "time15.4.txt");
+}
+
+#[test]
+fn time15_5() {
+ test_times(Format::new(Type::Time, 15, 5).unwrap(), "time15.5.txt");
+}
+
+#[test]
+fn time15_6() {
+ test_times(Format::new(Type::Time, 15, 6).unwrap(), "time15.6.txt");
+}
+
+#[test]
+fn mtime5() {
+ test_times(Format::new(Type::MTime, 5, 0).unwrap(), "mtime5.txt");
+}
+
+#[test]
+fn mtime6() {
+ test_times(Format::new(Type::MTime, 6, 0).unwrap(), "mtime6.txt");
+}
+
+#[test]
+fn mtime7() {
+ test_times(Format::new(Type::MTime, 7, 0).unwrap(), "mtime7.txt");
+}
+
+#[test]
+fn mtime7_1() {
+ test_times(Format::new(Type::MTime, 7, 1).unwrap(), "mtime7.1.txt");
+}
+
+#[test]
+fn mtime8() {
+ test_times(Format::new(Type::MTime, 8, 0).unwrap(), "mtime8.txt");
+}
+
+#[test]
+fn mtime8_1() {
+ test_times(Format::new(Type::MTime, 8, 1).unwrap(), "mtime8.1.txt");
+}
+
+#[test]
+fn mtime8_2() {
+ test_times(Format::new(Type::MTime, 8, 2).unwrap(), "mtime8.2.txt");
+}
+
+#[test]
+fn mtime9() {
+ test_times(Format::new(Type::MTime, 9, 0).unwrap(), "mtime9.txt");
+}
+
+#[test]
+fn mtime9_1() {
+ test_times(Format::new(Type::MTime, 9, 1).unwrap(), "mtime9.1.txt");
+}
+
+#[test]
+fn mtime9_2() {
+ test_times(Format::new(Type::MTime, 9, 2).unwrap(), "mtime9.2.txt");
+}
+
+#[test]
+fn mtime9_3() {
+ test_times(Format::new(Type::MTime, 9, 3).unwrap(), "mtime9.3.txt");
+}
+
+#[test]
+fn mtime10() {
+ test_times(Format::new(Type::MTime, 10, 0).unwrap(), "mtime10.txt");
+}
+
+#[test]
+fn mtime10_1() {
+ test_times(Format::new(Type::MTime, 10, 1).unwrap(), "mtime10.1.txt");
+}
+
+#[test]
+fn mtime10_2() {
+ test_times(Format::new(Type::MTime, 10, 2).unwrap(), "mtime10.2.txt");
+}
+
+#[test]
+fn mtime10_3() {
+ test_times(Format::new(Type::MTime, 10, 3).unwrap(), "mtime10.3.txt");
+}
+
+#[test]
+fn mtime10_4() {
+ test_times(Format::new(Type::MTime, 10, 4).unwrap(), "mtime10.4.txt");
+}
+
+#[test]
+fn mtime11() {
+ test_times(Format::new(Type::MTime, 11, 0).unwrap(), "mtime11.txt");
+}
+
+#[test]
+fn mtime11_1() {
+ test_times(Format::new(Type::MTime, 11, 1).unwrap(), "mtime11.1.txt");
+}
+
+#[test]
+fn mtime11_2() {
+ test_times(Format::new(Type::MTime, 11, 2).unwrap(), "mtime11.2.txt");
+}
+
+#[test]
+fn mtime11_3() {
+ test_times(Format::new(Type::MTime, 11, 3).unwrap(), "mtime11.3.txt");
+}
+
+#[test]
+fn mtime11_4() {
+ test_times(Format::new(Type::MTime, 11, 4).unwrap(), "mtime11.4.txt");
+}
+
+#[test]
+fn mtime11_5() {
+ test_times(Format::new(Type::MTime, 11, 5).unwrap(), "mtime11.5.txt");
+}
+
+#[test]
+fn mtime12_5() {
+ test_times(Format::new(Type::MTime, 12, 5).unwrap(), "mtime12.5.txt");
+}
+
+#[test]
+fn mtime13_5() {
+ test_times(Format::new(Type::MTime, 13, 5).unwrap(), "mtime13.5.txt");
+}
+
+#[test]
+fn mtime14_5() {
+ test_times(Format::new(Type::MTime, 14, 5).unwrap(), "mtime14.5.txt");
+}
+
+#[test]
+fn mtime15_5() {
+ test_times(Format::new(Type::MTime, 15, 5).unwrap(), "mtime15.5.txt");
+}
+
+#[test]
+fn mtime16_5() {
+ test_times(Format::new(Type::MTime, 16, 5).unwrap(), "mtime16.5.txt");
+}
+
+#[test]
+fn dtime8() {
+ test_times(Format::new(Type::DTime, 8, 0).unwrap(), "dtime8.txt");
+}
+
+#[test]
+fn dtime9() {
+ test_times(Format::new(Type::DTime, 9, 0).unwrap(), "dtime9.txt");
+}
+
+#[test]
+fn dtime10() {
+ test_times(Format::new(Type::DTime, 10, 0).unwrap(), "dtime10.txt");
+}
+
+#[test]
+fn dtime11() {
+ test_times(Format::new(Type::DTime, 11, 0).unwrap(), "dtime11.txt");
+}
+
+#[test]
+fn dtime12() {
+ test_times(Format::new(Type::DTime, 12, 0).unwrap(), "dtime12.txt");
+}
+
+#[test]
+fn dtime13() {
+ test_times(Format::new(Type::DTime, 13, 0).unwrap(), "dtime13.txt");
+}
+
+#[test]
+fn dtime13_1() {
+ test_times(Format::new(Type::DTime, 13, 1).unwrap(), "dtime13.1.txt");
+}
+
+#[test]
+fn dtime14() {
+ test_times(Format::new(Type::DTime, 14, 0).unwrap(), "dtime14.txt");
+}
+
+#[test]
+fn dtime14_1() {
+ test_times(Format::new(Type::DTime, 14, 1).unwrap(), "dtime14.1.txt");
+}
+
+#[test]
+fn dtime14_2() {
+ test_times(Format::new(Type::DTime, 14, 2).unwrap(), "dtime14.2.txt");
+}
+
+#[test]
+fn dtime15() {
+ test_times(Format::new(Type::DTime, 15, 0).unwrap(), "dtime15.txt");
+}
+
+#[test]
+fn dtime15_1() {
+ test_times(Format::new(Type::DTime, 15, 1).unwrap(), "dtime15.1.txt");
+}
+
+#[test]
+fn dtime15_2() {
+ test_times(Format::new(Type::DTime, 15, 2).unwrap(), "dtime15.2.txt");
+}
+
+#[test]
+fn dtime15_3() {
+ test_times(Format::new(Type::DTime, 15, 3).unwrap(), "dtime15.3.txt");
+}
+
+#[test]
+fn dtime16() {
+ test_times(Format::new(Type::DTime, 16, 0).unwrap(), "dtime16.txt");
+}
+
+#[test]
+fn dtime16_1() {
+ test_times(Format::new(Type::DTime, 16, 1).unwrap(), "dtime16.1.txt");
+}
+
+#[test]
+fn dtime16_2() {
+ test_times(Format::new(Type::DTime, 16, 2).unwrap(), "dtime16.2.txt");
+}
+
+#[test]
+fn dtime16_3() {
+ test_times(Format::new(Type::DTime, 16, 3).unwrap(), "dtime16.3.txt");
+}
+
+#[test]
+fn dtime16_4() {
+ test_times(Format::new(Type::DTime, 16, 4).unwrap(), "dtime16.4.txt");
+}
+
+#[test]
+fn dtime17() {
+ test_times(Format::new(Type::DTime, 17, 0).unwrap(), "dtime17.txt");
+}
+
+#[test]
+fn dtime17_1() {
+ test_times(Format::new(Type::DTime, 17, 1).unwrap(), "dtime17.1.txt");
+}
+
+#[test]
+fn dtime17_2() {
+ test_times(Format::new(Type::DTime, 17, 2).unwrap(), "dtime17.2.txt");
+}
+
+#[test]
+fn dtime17_3() {
+ test_times(Format::new(Type::DTime, 17, 3).unwrap(), "dtime17.3.txt");
+}
+
+#[test]
+fn dtime17_4() {
+ test_times(Format::new(Type::DTime, 17, 4).unwrap(), "dtime17.4.txt");
+}
+
+#[test]
+fn dtime17_5() {
+ test_times(Format::new(Type::DTime, 17, 5).unwrap(), "dtime17.5.txt");
+}
+
+#[test]
+fn dtime18() {
+ test_times(Format::new(Type::DTime, 18, 0).unwrap(), "dtime18.txt");
+}
+
+#[test]
+fn dtime18_1() {
+ test_times(Format::new(Type::DTime, 18, 1).unwrap(), "dtime18.1.txt");
+}
+
+#[test]
+fn dtime18_2() {
+ test_times(Format::new(Type::DTime, 18, 2).unwrap(), "dtime18.2.txt");
+}
+
+#[test]
+fn dtime18_3() {
+ test_times(Format::new(Type::DTime, 18, 3).unwrap(), "dtime18.3.txt");
+}
+
+#[test]
+fn dtime18_4() {
+ test_times(Format::new(Type::DTime, 18, 4).unwrap(), "dtime18.4.txt");
+}
+
+#[test]
+fn dtime18_5() {
+ test_times(Format::new(Type::DTime, 18, 5).unwrap(), "dtime18.5.txt");
+}
+
+#[test]
+fn dtime18_6() {
+ test_times(Format::new(Type::DTime, 18, 6).unwrap(), "dtime18.6.txt");
+}
}
#[cfg(test)]
-mod test {
+mod tests {
use std::{
fs::File,
io::{BufRead, BufReader},
}
#[cfg(test)]
-mod test;
+mod tests;
+++ /dev/null
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program. If not, see <http://www.gnu.org/licenses/>.
-
-use crate::{
- identifier::Identifier,
- lex::{
- segment::Syntax,
- token::{Punct, Token},
- },
-};
-
-use super::{ScanError, StringScanner};
-
-fn print_token(token: &Token) {
- match token {
- Token::Id(s) => print!("Token::Id(String::from({s:?}))"),
- Token::Number(number) => print!("Token::Number({number:?})"),
- Token::String(s) => print!("Token::String(String::from({s:?}))"),
- Token::End => print!("Token::EndCommand"),
- Token::Punct(punct) => print!("Token::Punct(Punct::{punct:?})"),
- }
-}
-
-#[track_caller]
-fn check_scan(input: &str, mode: Syntax, expected: &[Result<Token, ScanError>]) {
- let tokens = StringScanner::new(input, mode, false).collect::<Vec<_>>();
-
- if tokens != expected {
- for token in &tokens {
- match token {
- Ok(token) => {
- print!("Ok(");
- print_token(token);
- print!(")");
- }
- Err(error) => print!("Err(ScanError::{error:?})"),
- }
- println!(",");
- }
-
- eprintln!("tokens differ from expected:");
- let difference = diff::slice(expected, &tokens);
- for result in difference {
- match result {
- diff::Result::Left(left) => eprintln!("-{left:?}"),
- diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
- diff::Result::Right(right) => eprintln!("+{right:?}"),
- }
- }
- panic!();
- }
-}
-
-#[test]
-fn test_identifiers() {
- check_scan(
- r#"a aB i5 $x @efg @@. !abcd !* !*a #.# .x _z.
-abcd. abcd.
-QRSTUV./* end of line comment */
-QrStUv./* end of line comment */
-WXYZ. /* unterminated end of line comment
-�. /* U+FFFD is not valid in an identifier
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("a").unwrap())),
- Ok(Token::Id(Identifier::new("aB").unwrap())),
- Ok(Token::Id(Identifier::new("i5").unwrap())),
- Ok(Token::Id(Identifier::new("$x").unwrap())),
- Ok(Token::Id(Identifier::new("@efg").unwrap())),
- Ok(Token::Id(Identifier::new("@@.").unwrap())),
- Ok(Token::Id(Identifier::new("!abcd").unwrap())),
- Ok(Token::Punct(Punct::BangAsterisk)),
- Ok(Token::Punct(Punct::BangAsterisk)),
- Ok(Token::Id(Identifier::new("a").unwrap())),
- Ok(Token::Id(Identifier::new("#.#").unwrap())),
- Ok(Token::Punct(Punct::Dot)),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Punct(Punct::Underscore)),
- Ok(Token::Id(Identifier::new("z").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("abcd.").unwrap())),
- Ok(Token::Id(Identifier::new("abcd").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("QRSTUV").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("QrStUv").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("WXYZ").unwrap())),
- Ok(Token::End),
- Err(ScanError::UnexpectedChar('�')),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_reserved_words() {
- check_scan(
- r#"and or not eq ge gt le lt ne all by to with
-AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
-andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
-and. with.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Punct(Punct::And)),
- Ok(Token::Punct(Punct::Or)),
- Ok(Token::Punct(Punct::Not)),
- Ok(Token::Punct(Punct::Eq)),
- Ok(Token::Punct(Punct::Ge)),
- Ok(Token::Punct(Punct::Gt)),
- Ok(Token::Punct(Punct::Le)),
- Ok(Token::Punct(Punct::Lt)),
- Ok(Token::Punct(Punct::Ne)),
- Ok(Token::Punct(Punct::All)),
- Ok(Token::Punct(Punct::By)),
- Ok(Token::Punct(Punct::To)),
- Ok(Token::Punct(Punct::With)),
- Ok(Token::Punct(Punct::And)),
- Ok(Token::Punct(Punct::Or)),
- Ok(Token::Punct(Punct::Not)),
- Ok(Token::Punct(Punct::Eq)),
- Ok(Token::Punct(Punct::Ge)),
- Ok(Token::Punct(Punct::Gt)),
- Ok(Token::Punct(Punct::Le)),
- Ok(Token::Punct(Punct::Lt)),
- Ok(Token::Punct(Punct::Ne)),
- Ok(Token::Punct(Punct::All)),
- Ok(Token::Punct(Punct::By)),
- Ok(Token::Punct(Punct::To)),
- Ok(Token::Punct(Punct::With)),
- Ok(Token::Id(Identifier::new("andx").unwrap())),
- Ok(Token::Id(Identifier::new("orx").unwrap())),
- Ok(Token::Id(Identifier::new("notx").unwrap())),
- Ok(Token::Id(Identifier::new("eqx").unwrap())),
- Ok(Token::Id(Identifier::new("gex").unwrap())),
- Ok(Token::Id(Identifier::new("gtx").unwrap())),
- Ok(Token::Id(Identifier::new("lex").unwrap())),
- Ok(Token::Id(Identifier::new("ltx").unwrap())),
- Ok(Token::Id(Identifier::new("nex").unwrap())),
- Ok(Token::Id(Identifier::new("allx").unwrap())),
- Ok(Token::Id(Identifier::new("byx").unwrap())),
- Ok(Token::Id(Identifier::new("tox").unwrap())),
- Ok(Token::Id(Identifier::new("withx").unwrap())),
- Ok(Token::Id(Identifier::new("and.").unwrap())),
- Ok(Token::Punct(Punct::With)),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_punctuation() {
- check_scan(
- r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
-~&|=>=><=<~=<>(),-+*/[]**
-% : ; ? _ ` { } ~
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Punct(Punct::Not)),
- Ok(Token::Punct(Punct::And)),
- Ok(Token::Punct(Punct::Or)),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Punct(Punct::Ge)),
- Ok(Token::Punct(Punct::Gt)),
- Ok(Token::Punct(Punct::Le)),
- Ok(Token::Punct(Punct::Lt)),
- Ok(Token::Punct(Punct::Ne)),
- Ok(Token::Punct(Punct::Ne)),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Punct(Punct::Dash)),
- Ok(Token::Punct(Punct::Plus)),
- Ok(Token::Punct(Punct::Asterisk)),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Punct(Punct::LSquare)),
- Ok(Token::Punct(Punct::RSquare)),
- Ok(Token::Punct(Punct::Exp)),
- Ok(Token::Punct(Punct::Not)),
- Ok(Token::Punct(Punct::And)),
- Ok(Token::Punct(Punct::Or)),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Punct(Punct::Ge)),
- Ok(Token::Punct(Punct::Gt)),
- Ok(Token::Punct(Punct::Le)),
- Ok(Token::Punct(Punct::Lt)),
- Ok(Token::Punct(Punct::Ne)),
- Ok(Token::Punct(Punct::Ne)),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Punct(Punct::Dash)),
- Ok(Token::Punct(Punct::Plus)),
- Ok(Token::Punct(Punct::Asterisk)),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Punct(Punct::LSquare)),
- Ok(Token::Punct(Punct::RSquare)),
- Ok(Token::Punct(Punct::Exp)),
- Ok(Token::Punct(Punct::Percent)),
- Ok(Token::Punct(Punct::Colon)),
- Ok(Token::Punct(Punct::Semicolon)),
- Ok(Token::Punct(Punct::Question)),
- Ok(Token::Punct(Punct::Underscore)),
- Ok(Token::Punct(Punct::Backtick)),
- Ok(Token::Punct(Punct::LCurly)),
- Ok(Token::Punct(Punct::RCurly)),
- Ok(Token::Punct(Punct::Not)),
- ],
- );
-}
-
-#[test]
-fn test_positive_numbers() {
- check_scan(
- r#"0 1 01 001. 1.
-123. /* comment 1 */ /* comment 2 */
-.1 0.1 00.1 00.10
-5e1 6E-1 7e+1 6E+01 6e-03
-.3E1 .4e-1 .5E+1 .6e+01 .7E-03
-1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
-. 1e e1 1e+ 1e-
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Number(0.0)),
- Ok(Token::Number(1.0)),
- Ok(Token::Number(1.0)),
- Ok(Token::Number(1.0)),
- Ok(Token::Number(1.0)),
- Ok(Token::End),
- Ok(Token::Number(123.0)),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::Number(1.0)),
- Ok(Token::Number(0.1)),
- Ok(Token::Number(0.1)),
- Ok(Token::Number(0.1)),
- Ok(Token::Number(50.0)),
- Ok(Token::Number(0.6)),
- Ok(Token::Number(70.0)),
- Ok(Token::Number(60.0)),
- Ok(Token::Number(0.006)),
- Ok(Token::End),
- Ok(Token::Number(30.0)),
- Ok(Token::Number(0.04)),
- Ok(Token::Number(5.0)),
- Ok(Token::Number(6.0)),
- Ok(Token::Number(0.0007)),
- Ok(Token::Number(12.3)),
- Ok(Token::Number(4.56)),
- Ok(Token::Number(789.0)),
- Ok(Token::Number(999.0)),
- Ok(Token::Number(0.0112)),
- Ok(Token::End),
- Err(ScanError::ExpectedExponent(String::from("1e"))),
- Ok(Token::Id(Identifier::new("e1").unwrap())),
- Err(ScanError::ExpectedExponent(String::from("1e+"))),
- Err(ScanError::ExpectedExponent(String::from("1e-"))),
- ],
- );
-}
-
-#[test]
-fn test_negative_numbers() {
- check_scan(
- r#" -0 -1 -01 -001. -1.
- -123. /* comment 1 */ /* comment 2 */
- -.1 -0.1 -00.1 -00.10
- -5e1 -6E-1 -7e+1 -6E+01 -6e-03
- -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
- -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
- -/**/1
- -. -1e -e1 -1e+ -1e- -1.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Number(-0.0)),
- Ok(Token::Number(-1.0)),
- Ok(Token::Number(-1.0)),
- Ok(Token::Number(-1.0)),
- Ok(Token::Number(-1.0)),
- Ok(Token::End),
- Ok(Token::Number(-123.0)),
- Ok(Token::End),
- Ok(Token::Number(-0.1)),
- Ok(Token::Number(-0.1)),
- Ok(Token::Number(-0.1)),
- Ok(Token::Number(-0.1)),
- Ok(Token::Number(-50.0)),
- Ok(Token::Number(-0.6)),
- Ok(Token::Number(-70.0)),
- Ok(Token::Number(-60.0)),
- Ok(Token::Number(-0.006)),
- Ok(Token::Number(-3.0)),
- Ok(Token::Number(-0.04)),
- Ok(Token::Number(-5.0)),
- Ok(Token::Number(-6.0)),
- Ok(Token::Number(-0.0007)),
- Ok(Token::Number(-12.3)),
- Ok(Token::Number(-4.56)),
- Ok(Token::Number(-789.0)),
- Ok(Token::Number(-999.0)),
- Ok(Token::Number(-0.0112)),
- Ok(Token::Number(-1.0)),
- Ok(Token::Punct(Punct::Dash)),
- Ok(Token::Punct(Punct::Dot)),
- Err(ScanError::ExpectedExponent(String::from("-1e"))),
- Ok(Token::Punct(Punct::Dash)),
- Ok(Token::Id(Identifier::new("e1").unwrap())),
- Err(ScanError::ExpectedExponent(String::from("-1e+"))),
- Err(ScanError::ExpectedExponent(String::from("-1e-"))),
- Ok(Token::Number(-1.0)),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_strings() {
- check_scan(
- r#"'x' "y" 'abc'
-'Don''t' "Can't" 'Won''t'
-"""quoted""" '"quoted"'
-'' "" '''' """"
-'missing end quote
-"missing double quote
-'x' + "y"
-+ 'z' +
-'a' /* abc */ + "b" /*
-+ 'c' +/* */"d"/* */+'e'
-'foo'
-+ /* special case: + in column 0 would ordinarily start a new command
-'bar'
-'foo'
- +
-'bar'
-'foo'
-+
-
-'bar'
-
-+
-x"4142"+'5152'
-"4142"+
-x'5152'
-x"4142"
-+u'304a'
-"�あいうえお"
-"abc"+U"FFFD"+u'3048'+"xyz"
-"#,
- Syntax::Auto,
- &[
- Ok(Token::String(String::from("x"))),
- Ok(Token::String(String::from("y"))),
- Ok(Token::String(String::from("abc"))),
- Ok(Token::String(String::from("Don't"))),
- Ok(Token::String(String::from("Can't"))),
- Ok(Token::String(String::from("Won't"))),
- Ok(Token::String(String::from("\"quoted\""))),
- Ok(Token::String(String::from("\"quoted\""))),
- Ok(Token::String(String::from(""))),
- Ok(Token::String(String::from(""))),
- Ok(Token::String(String::from("'"))),
- Ok(Token::String(String::from("\""))),
- Err(ScanError::ExpectedQuote),
- Err(ScanError::ExpectedQuote),
- Ok(Token::String(String::from("xyzabcde"))),
- Ok(Token::String(String::from("foobar"))),
- Ok(Token::String(String::from("foobar"))),
- Ok(Token::String(String::from("foo"))),
- Ok(Token::Punct(Punct::Plus)),
- Ok(Token::End),
- Ok(Token::String(String::from("bar"))),
- Ok(Token::End),
- Ok(Token::Punct(Punct::Plus)),
- Ok(Token::String(String::from("AB5152"))),
- Ok(Token::String(String::from("4142QR"))),
- Ok(Token::String(String::from("ABお"))),
- Ok(Token::String(String::from("�あいうえお"))),
- Ok(Token::String(String::from("abc�えxyz"))),
- ],
- );
-}
-
-#[test]
-fn test_shbang() {
- check_scan(
- r#"#! /usr/bin/pspp
-#! /usr/bin/pspp
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("#").unwrap())),
- Ok(Token::Punct(Punct::Bang)),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("usr").unwrap())),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("bin").unwrap())),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("pspp").unwrap())),
- ],
- );
-}
-
-#[test]
-fn test_comments() {
- check_scan(
- r#"* Comment commands "don't
-have to contain valid tokens.
-
-** Check ambiguity with ** token.
-****************.
-
-comment keyword works too.
-COMM also.
-com is ambiguous with COMPUTE.
-
- * Comment need not start at left margin.
-
-* Comment ends with blank line
-
-next command.
-
-"#,
- Syntax::Auto,
- &[
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("com").unwrap())),
- Ok(Token::Id(Identifier::new("is").unwrap())),
- Ok(Token::Id(Identifier::new("ambiguous").unwrap())),
- Ok(Token::Punct(Punct::With)),
- Ok(Token::Id(Identifier::new("COMPUTE").unwrap())),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("next").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::End),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_document() {
- check_scan(
- r#"DOCUMENT one line.
-DOC more
- than
- one
- line.
-docu
-first.paragraph
-isn't parsed as tokens
-
-second paragraph.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
- Ok(Token::String(String::from("DOCUMENT one line."))),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
- Ok(Token::String(String::from("DOC more"))),
- Ok(Token::String(String::from(" than"))),
- Ok(Token::String(String::from(" one"))),
- Ok(Token::String(String::from(" line."))),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
- Ok(Token::String(String::from("docu"))),
- Ok(Token::String(String::from("first.paragraph"))),
- Ok(Token::String(String::from("isn't parsed as tokens"))),
- Ok(Token::String(String::from(""))),
- Ok(Token::String(String::from("second paragraph."))),
- Ok(Token::End),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_file_label() {
- check_scan(
- r#"FIL label isn't quoted.
-FILE
- lab 'is quoted'.
-FILE /*
-/**/ lab not quoted here either
-
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("FIL").unwrap())),
- Ok(Token::Id(Identifier::new("label").unwrap())),
- Ok(Token::String(String::from("isn't quoted"))),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("FILE").unwrap())),
- Ok(Token::Id(Identifier::new("lab").unwrap())),
- Ok(Token::String(String::from("is quoted"))),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("FILE").unwrap())),
- Ok(Token::Id(Identifier::new("lab").unwrap())),
- Ok(Token::String(String::from("not quoted here either"))),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_begin_data() {
- check_scan(
- r#"begin data.
-123
-xxx
-end data.
-
-BEG /**/ DAT /*
-5 6 7 /* x
-
-end data
-end data
-.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("begin").unwrap())),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::End),
- Ok(Token::String(String::from("123"))),
- Ok(Token::String(String::from("xxx"))),
- Ok(Token::Id(Identifier::new("end").unwrap())),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::End),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("BEG").unwrap())),
- Ok(Token::Id(Identifier::new("DAT").unwrap())),
- Ok(Token::String(String::from("5 6 7 /* x"))),
- Ok(Token::String(String::from(""))),
- Ok(Token::String(String::from("end data"))),
- Ok(Token::Id(Identifier::new("end").unwrap())),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_do_repeat() {
- check_scan(
- r#"do repeat x=a b c
- y=d e f.
- do repeat a=1 thru 5.
-another command.
-second command
-+ third command.
-end /* x */ /* y */ repeat print.
-end
- repeat.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("do").unwrap())),
- Ok(Token::Id(Identifier::new("repeat").unwrap())),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Id(Identifier::new("a").unwrap())),
- Ok(Token::Id(Identifier::new("b").unwrap())),
- Ok(Token::Id(Identifier::new("c").unwrap())),
- Ok(Token::Id(Identifier::new("y").unwrap())),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Id(Identifier::new("d").unwrap())),
- Ok(Token::Id(Identifier::new("e").unwrap())),
- Ok(Token::Id(Identifier::new("f").unwrap())),
- Ok(Token::End),
- Ok(Token::String(String::from(" do repeat a=1 thru 5."))),
- Ok(Token::String(String::from("another command."))),
- Ok(Token::String(String::from("second command"))),
- Ok(Token::String(String::from("+ third command."))),
- Ok(Token::String(String::from(
- "end /* x */ /* y */ repeat print.",
- ))),
- Ok(Token::Id(Identifier::new("end").unwrap())),
- Ok(Token::Id(Identifier::new("repeat").unwrap())),
- Ok(Token::End),
- ],
- );
-}
-
-#[test]
-fn test_do_repeat_batch() {
- check_scan(
- r#"do repeat x=a b c
- y=d e f
-do repeat a=1 thru 5
-another command
-second command
-+ third command
-end /* x */ /* y */ repeat print
-end
- repeat
-do
- repeat #a=1
-
- inner command
-end repeat
-"#,
- Syntax::Batch,
- &[
- Ok(Token::Id(Identifier::new("do").unwrap())),
- Ok(Token::Id(Identifier::new("repeat").unwrap())),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Id(Identifier::new("a").unwrap())),
- Ok(Token::Id(Identifier::new("b").unwrap())),
- Ok(Token::Id(Identifier::new("c").unwrap())),
- Ok(Token::Id(Identifier::new("y").unwrap())),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Id(Identifier::new("d").unwrap())),
- Ok(Token::Id(Identifier::new("e").unwrap())),
- Ok(Token::Id(Identifier::new("f").unwrap())),
- Ok(Token::End),
- Ok(Token::String(String::from("do repeat a=1 thru 5"))),
- Ok(Token::String(String::from("another command"))),
- Ok(Token::String(String::from("second command"))),
- Ok(Token::String(String::from("+ third command"))),
- Ok(Token::String(String::from(
- "end /* x */ /* y */ repeat print",
- ))),
- Ok(Token::Id(Identifier::new("end").unwrap())),
- Ok(Token::Id(Identifier::new("repeat").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("do").unwrap())),
- Ok(Token::Id(Identifier::new("repeat").unwrap())),
- Ok(Token::Id(Identifier::new("#a").unwrap())),
- Ok(Token::Punct(Punct::Equals)),
- Ok(Token::Number(1.0)),
- Ok(Token::End),
- Ok(Token::String(String::from(" inner command"))),
- Ok(Token::Id(Identifier::new("end").unwrap())),
- Ok(Token::Id(Identifier::new("repeat").unwrap())),
- ],
- );
-}
-
-#[test]
-fn test_batch_mode() {
- check_scan(
- r#"first command
- another line of first command
-+ second command
-third command
-
-fourth command.
- fifth command.
-"#,
- Syntax::Batch,
- &[
- Ok(Token::Id(Identifier::new("first").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::Id(Identifier::new("another").unwrap())),
- Ok(Token::Id(Identifier::new("line").unwrap())),
- Ok(Token::Id(Identifier::new("of").unwrap())),
- Ok(Token::Id(Identifier::new("first").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("second").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("third").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("fourth").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("fifth").unwrap())),
- Ok(Token::Id(Identifier::new("command").unwrap())),
- Ok(Token::End),
- ],
- );
-}
-
-mod define {
- use crate::{
- identifier::Identifier,
- lex::{
- segment::Syntax,
- token::{Punct, Token},
- },
- };
-
- use super::check_scan;
-
- #[test]
- fn test_simple() {
- check_scan(
- r#"define !macro1()
-var1 var2 var3
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from("var1 var2 var3"))),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_no_newline_after_parentheses() {
- check_scan(
- r#"define !macro1() var1 var2 var3
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from(" var1 var2 var3"))),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_no_newline_before_enddefine() {
- check_scan(
- r#"define !macro1()
-var1 var2 var3!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from("var1 var2 var3"))),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_all_on_one_line() {
- check_scan(
- r#"define !macro1()var1 var2 var3!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from("var1 var2 var3"))),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_empty() {
- check_scan(
- r#"define !macro1()
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_blank_lines() {
- check_scan(
- r#"define !macro1()
-
-
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from(""))),
- Ok(Token::String(String::from(""))),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_arguments() {
- check_scan(
- r#"define !macro1(a(), b(), c())
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Id(Identifier::new("a").unwrap())),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Id(Identifier::new("b").unwrap())),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Id(Identifier::new("c").unwrap())),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_multiline_arguments() {
- check_scan(
- r#"define !macro1(
- a(), b(
- ),
- c()
-)
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Id(Identifier::new("a").unwrap())),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Id(Identifier::new("b").unwrap())),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Id(Identifier::new("c").unwrap())),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_arguments_start_on_second_line() {
- check_scan(
- r#"define !macro1
-(x,y,z
-)
-content 1
-content 2
-!enddefine.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Id(Identifier::new("y").unwrap())),
- Ok(Token::Punct(Punct::Comma)),
- Ok(Token::Id(Identifier::new("z").unwrap())),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from("content 1"))),
- Ok(Token::String(String::from("content 2"))),
- Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_early_end_of_command_1() {
- check_scan(
- r#"define !macro1.
-data list /x 1.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::Id(Identifier::new("list").unwrap())),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Number(1.0)),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_early_end_of_command_2() {
- check_scan(
- r#"define !macro1
-x.
-data list /x 1.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::Id(Identifier::new("list").unwrap())),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Number(1.0)),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_early_end_of_command_3() {
- check_scan(
- r#"define !macro1(.
-x.
-data list /x 1.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::Id(Identifier::new("list").unwrap())),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Number(1.0)),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_early_end_of_command_4() {
- // Notice the command terminator at the end of the DEFINE command,
- // which should not be there and ends it early.
- check_scan(
- r#"define !macro1.
-data list /x 1.
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::End),
- Ok(Token::Id(Identifier::new("data").unwrap())),
- Ok(Token::Id(Identifier::new("list").unwrap())),
- Ok(Token::Punct(Punct::Slash)),
- Ok(Token::Id(Identifier::new("x").unwrap())),
- Ok(Token::Number(1.0)),
- Ok(Token::End),
- ],
- );
- }
-
- #[test]
- fn test_missing_enddefine() {
- check_scan(
- r#"define !macro1()
-content line 1
-content line 2
-"#,
- Syntax::Auto,
- &[
- Ok(Token::Id(Identifier::new("define").unwrap())),
- Ok(Token::String(String::from("!macro1"))),
- Ok(Token::Punct(Punct::LParen)),
- Ok(Token::Punct(Punct::RParen)),
- Ok(Token::String(String::from("content line 1"))),
- Ok(Token::String(String::from("content line 2"))),
- ],
- );
- }
-}
--- /dev/null
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see <http://www.gnu.org/licenses/>.
+
+use crate::{
+ identifier::Identifier,
+ lex::{
+ segment::Syntax,
+ token::{Punct, Token},
+ },
+};
+
+use super::{ScanError, StringScanner};
+
+fn print_token(token: &Token) {
+ match token {
+ Token::Id(s) => print!("Token::Id(String::from({s:?}))"),
+ Token::Number(number) => print!("Token::Number({number:?})"),
+ Token::String(s) => print!("Token::String(String::from({s:?}))"),
+ Token::End => print!("Token::EndCommand"),
+ Token::Punct(punct) => print!("Token::Punct(Punct::{punct:?})"),
+ }
+}
+
+#[track_caller]
+fn check_scan(input: &str, mode: Syntax, expected: &[Result<Token, ScanError>]) {
+ let tokens = StringScanner::new(input, mode, false).collect::<Vec<_>>();
+
+ if tokens != expected {
+ for token in &tokens {
+ match token {
+ Ok(token) => {
+ print!("Ok(");
+ print_token(token);
+ print!(")");
+ }
+ Err(error) => print!("Err(ScanError::{error:?})"),
+ }
+ println!(",");
+ }
+
+ eprintln!("tokens differ from expected:");
+ let difference = diff::slice(expected, &tokens);
+ for result in difference {
+ match result {
+ diff::Result::Left(left) => eprintln!("-{left:?}"),
+ diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
+ diff::Result::Right(right) => eprintln!("+{right:?}"),
+ }
+ }
+ panic!();
+ }
+}
+
+#[test]
+fn test_identifiers() {
+ check_scan(
+ r#"a aB i5 $x @efg @@. !abcd !* !*a #.# .x _z.
+abcd. abcd.
+QRSTUV./* end of line comment */
+QrStUv./* end of line comment */
+WXYZ. /* unterminated end of line comment
+�. /* U+FFFD is not valid in an identifier
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("a").unwrap())),
+ Ok(Token::Id(Identifier::new("aB").unwrap())),
+ Ok(Token::Id(Identifier::new("i5").unwrap())),
+ Ok(Token::Id(Identifier::new("$x").unwrap())),
+ Ok(Token::Id(Identifier::new("@efg").unwrap())),
+ Ok(Token::Id(Identifier::new("@@.").unwrap())),
+ Ok(Token::Id(Identifier::new("!abcd").unwrap())),
+ Ok(Token::Punct(Punct::BangAsterisk)),
+ Ok(Token::Punct(Punct::BangAsterisk)),
+ Ok(Token::Id(Identifier::new("a").unwrap())),
+ Ok(Token::Id(Identifier::new("#.#").unwrap())),
+ Ok(Token::Punct(Punct::Dot)),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Punct(Punct::Underscore)),
+ Ok(Token::Id(Identifier::new("z").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("abcd.").unwrap())),
+ Ok(Token::Id(Identifier::new("abcd").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("QRSTUV").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("QrStUv").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("WXYZ").unwrap())),
+ Ok(Token::End),
+ Err(ScanError::UnexpectedChar('�')),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_reserved_words() {
+ check_scan(
+ r#"and or not eq ge gt le lt ne all by to with
+AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
+andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
+and. with.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Punct(Punct::And)),
+ Ok(Token::Punct(Punct::Or)),
+ Ok(Token::Punct(Punct::Not)),
+ Ok(Token::Punct(Punct::Eq)),
+ Ok(Token::Punct(Punct::Ge)),
+ Ok(Token::Punct(Punct::Gt)),
+ Ok(Token::Punct(Punct::Le)),
+ Ok(Token::Punct(Punct::Lt)),
+ Ok(Token::Punct(Punct::Ne)),
+ Ok(Token::Punct(Punct::All)),
+ Ok(Token::Punct(Punct::By)),
+ Ok(Token::Punct(Punct::To)),
+ Ok(Token::Punct(Punct::With)),
+ Ok(Token::Punct(Punct::And)),
+ Ok(Token::Punct(Punct::Or)),
+ Ok(Token::Punct(Punct::Not)),
+ Ok(Token::Punct(Punct::Eq)),
+ Ok(Token::Punct(Punct::Ge)),
+ Ok(Token::Punct(Punct::Gt)),
+ Ok(Token::Punct(Punct::Le)),
+ Ok(Token::Punct(Punct::Lt)),
+ Ok(Token::Punct(Punct::Ne)),
+ Ok(Token::Punct(Punct::All)),
+ Ok(Token::Punct(Punct::By)),
+ Ok(Token::Punct(Punct::To)),
+ Ok(Token::Punct(Punct::With)),
+ Ok(Token::Id(Identifier::new("andx").unwrap())),
+ Ok(Token::Id(Identifier::new("orx").unwrap())),
+ Ok(Token::Id(Identifier::new("notx").unwrap())),
+ Ok(Token::Id(Identifier::new("eqx").unwrap())),
+ Ok(Token::Id(Identifier::new("gex").unwrap())),
+ Ok(Token::Id(Identifier::new("gtx").unwrap())),
+ Ok(Token::Id(Identifier::new("lex").unwrap())),
+ Ok(Token::Id(Identifier::new("ltx").unwrap())),
+ Ok(Token::Id(Identifier::new("nex").unwrap())),
+ Ok(Token::Id(Identifier::new("allx").unwrap())),
+ Ok(Token::Id(Identifier::new("byx").unwrap())),
+ Ok(Token::Id(Identifier::new("tox").unwrap())),
+ Ok(Token::Id(Identifier::new("withx").unwrap())),
+ Ok(Token::Id(Identifier::new("and.").unwrap())),
+ Ok(Token::Punct(Punct::With)),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_punctuation() {
+ check_scan(
+ r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
+~&|=>=><=<~=<>(),-+*/[]**
+% : ; ? _ ` { } ~
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Punct(Punct::Not)),
+ Ok(Token::Punct(Punct::And)),
+ Ok(Token::Punct(Punct::Or)),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Punct(Punct::Ge)),
+ Ok(Token::Punct(Punct::Gt)),
+ Ok(Token::Punct(Punct::Le)),
+ Ok(Token::Punct(Punct::Lt)),
+ Ok(Token::Punct(Punct::Ne)),
+ Ok(Token::Punct(Punct::Ne)),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Punct(Punct::Dash)),
+ Ok(Token::Punct(Punct::Plus)),
+ Ok(Token::Punct(Punct::Asterisk)),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Punct(Punct::LSquare)),
+ Ok(Token::Punct(Punct::RSquare)),
+ Ok(Token::Punct(Punct::Exp)),
+ Ok(Token::Punct(Punct::Not)),
+ Ok(Token::Punct(Punct::And)),
+ Ok(Token::Punct(Punct::Or)),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Punct(Punct::Ge)),
+ Ok(Token::Punct(Punct::Gt)),
+ Ok(Token::Punct(Punct::Le)),
+ Ok(Token::Punct(Punct::Lt)),
+ Ok(Token::Punct(Punct::Ne)),
+ Ok(Token::Punct(Punct::Ne)),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Punct(Punct::Dash)),
+ Ok(Token::Punct(Punct::Plus)),
+ Ok(Token::Punct(Punct::Asterisk)),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Punct(Punct::LSquare)),
+ Ok(Token::Punct(Punct::RSquare)),
+ Ok(Token::Punct(Punct::Exp)),
+ Ok(Token::Punct(Punct::Percent)),
+ Ok(Token::Punct(Punct::Colon)),
+ Ok(Token::Punct(Punct::Semicolon)),
+ Ok(Token::Punct(Punct::Question)),
+ Ok(Token::Punct(Punct::Underscore)),
+ Ok(Token::Punct(Punct::Backtick)),
+ Ok(Token::Punct(Punct::LCurly)),
+ Ok(Token::Punct(Punct::RCurly)),
+ Ok(Token::Punct(Punct::Not)),
+ ],
+ );
+}
+
+#[test]
+fn test_positive_numbers() {
+ check_scan(
+ r#"0 1 01 001. 1.
+123. /* comment 1 */ /* comment 2 */
+.1 0.1 00.1 00.10
+5e1 6E-1 7e+1 6E+01 6e-03
+.3E1 .4e-1 .5E+1 .6e+01 .7E-03
+1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
+. 1e e1 1e+ 1e-
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Number(0.0)),
+ Ok(Token::Number(1.0)),
+ Ok(Token::Number(1.0)),
+ Ok(Token::Number(1.0)),
+ Ok(Token::Number(1.0)),
+ Ok(Token::End),
+ Ok(Token::Number(123.0)),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::Number(1.0)),
+ Ok(Token::Number(0.1)),
+ Ok(Token::Number(0.1)),
+ Ok(Token::Number(0.1)),
+ Ok(Token::Number(50.0)),
+ Ok(Token::Number(0.6)),
+ Ok(Token::Number(70.0)),
+ Ok(Token::Number(60.0)),
+ Ok(Token::Number(0.006)),
+ Ok(Token::End),
+ Ok(Token::Number(30.0)),
+ Ok(Token::Number(0.04)),
+ Ok(Token::Number(5.0)),
+ Ok(Token::Number(6.0)),
+ Ok(Token::Number(0.0007)),
+ Ok(Token::Number(12.3)),
+ Ok(Token::Number(4.56)),
+ Ok(Token::Number(789.0)),
+ Ok(Token::Number(999.0)),
+ Ok(Token::Number(0.0112)),
+ Ok(Token::End),
+ Err(ScanError::ExpectedExponent(String::from("1e"))),
+ Ok(Token::Id(Identifier::new("e1").unwrap())),
+ Err(ScanError::ExpectedExponent(String::from("1e+"))),
+ Err(ScanError::ExpectedExponent(String::from("1e-"))),
+ ],
+ );
+}
+
+#[test]
+fn test_negative_numbers() {
+ check_scan(
+ r#" -0 -1 -01 -001. -1.
+ -123. /* comment 1 */ /* comment 2 */
+ -.1 -0.1 -00.1 -00.10
+ -5e1 -6E-1 -7e+1 -6E+01 -6e-03
+ -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
+ -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
+ -/**/1
+ -. -1e -e1 -1e+ -1e- -1.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Number(-0.0)),
+ Ok(Token::Number(-1.0)),
+ Ok(Token::Number(-1.0)),
+ Ok(Token::Number(-1.0)),
+ Ok(Token::Number(-1.0)),
+ Ok(Token::End),
+ Ok(Token::Number(-123.0)),
+ Ok(Token::End),
+ Ok(Token::Number(-0.1)),
+ Ok(Token::Number(-0.1)),
+ Ok(Token::Number(-0.1)),
+ Ok(Token::Number(-0.1)),
+ Ok(Token::Number(-50.0)),
+ Ok(Token::Number(-0.6)),
+ Ok(Token::Number(-70.0)),
+ Ok(Token::Number(-60.0)),
+ Ok(Token::Number(-0.006)),
+ Ok(Token::Number(-3.0)),
+ Ok(Token::Number(-0.04)),
+ Ok(Token::Number(-5.0)),
+ Ok(Token::Number(-6.0)),
+ Ok(Token::Number(-0.0007)),
+ Ok(Token::Number(-12.3)),
+ Ok(Token::Number(-4.56)),
+ Ok(Token::Number(-789.0)),
+ Ok(Token::Number(-999.0)),
+ Ok(Token::Number(-0.0112)),
+ Ok(Token::Number(-1.0)),
+ Ok(Token::Punct(Punct::Dash)),
+ Ok(Token::Punct(Punct::Dot)),
+ Err(ScanError::ExpectedExponent(String::from("-1e"))),
+ Ok(Token::Punct(Punct::Dash)),
+ Ok(Token::Id(Identifier::new("e1").unwrap())),
+ Err(ScanError::ExpectedExponent(String::from("-1e+"))),
+ Err(ScanError::ExpectedExponent(String::from("-1e-"))),
+ Ok(Token::Number(-1.0)),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_strings() {
+ check_scan(
+ r#"'x' "y" 'abc'
+'Don''t' "Can't" 'Won''t'
+"""quoted""" '"quoted"'
+'' "" '''' """"
+'missing end quote
+"missing double quote
+'x' + "y"
++ 'z' +
+'a' /* abc */ + "b" /*
++ 'c' +/* */"d"/* */+'e'
+'foo'
++ /* special case: + in column 0 would ordinarily start a new command
+'bar'
+'foo'
+ +
+'bar'
+'foo'
++
+
+'bar'
+
++
+x"4142"+'5152'
+"4142"+
+x'5152'
+x"4142"
++u'304a'
+"�あいうえお"
+"abc"+U"FFFD"+u'3048'+"xyz"
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::String(String::from("x"))),
+ Ok(Token::String(String::from("y"))),
+ Ok(Token::String(String::from("abc"))),
+ Ok(Token::String(String::from("Don't"))),
+ Ok(Token::String(String::from("Can't"))),
+ Ok(Token::String(String::from("Won't"))),
+ Ok(Token::String(String::from("\"quoted\""))),
+ Ok(Token::String(String::from("\"quoted\""))),
+ Ok(Token::String(String::from(""))),
+ Ok(Token::String(String::from(""))),
+ Ok(Token::String(String::from("'"))),
+ Ok(Token::String(String::from("\""))),
+ Err(ScanError::ExpectedQuote),
+ Err(ScanError::ExpectedQuote),
+ Ok(Token::String(String::from("xyzabcde"))),
+ Ok(Token::String(String::from("foobar"))),
+ Ok(Token::String(String::from("foobar"))),
+ Ok(Token::String(String::from("foo"))),
+ Ok(Token::Punct(Punct::Plus)),
+ Ok(Token::End),
+ Ok(Token::String(String::from("bar"))),
+ Ok(Token::End),
+ Ok(Token::Punct(Punct::Plus)),
+ Ok(Token::String(String::from("AB5152"))),
+ Ok(Token::String(String::from("4142QR"))),
+ Ok(Token::String(String::from("ABお"))),
+ Ok(Token::String(String::from("�あいうえお"))),
+ Ok(Token::String(String::from("abc�えxyz"))),
+ ],
+ );
+}
+
+#[test]
+fn test_shbang() {
+ check_scan(
+ r#"#! /usr/bin/pspp
+#! /usr/bin/pspp
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("#").unwrap())),
+ Ok(Token::Punct(Punct::Bang)),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("usr").unwrap())),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("bin").unwrap())),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("pspp").unwrap())),
+ ],
+ );
+}
+
+#[test]
+fn test_comments() {
+ check_scan(
+ r#"* Comment commands "don't
+have to contain valid tokens.
+
+** Check ambiguity with ** token.
+****************.
+
+comment keyword works too.
+COMM also.
+com is ambiguous with COMPUTE.
+
+ * Comment need not start at left margin.
+
+* Comment ends with blank line
+
+next command.
+
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("com").unwrap())),
+ Ok(Token::Id(Identifier::new("is").unwrap())),
+ Ok(Token::Id(Identifier::new("ambiguous").unwrap())),
+ Ok(Token::Punct(Punct::With)),
+ Ok(Token::Id(Identifier::new("COMPUTE").unwrap())),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("next").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::End),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_document() {
+ check_scan(
+ r#"DOCUMENT one line.
+DOC more
+ than
+ one
+ line.
+docu
+first.paragraph
+isn't parsed as tokens
+
+second paragraph.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
+ Ok(Token::String(String::from("DOCUMENT one line."))),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
+ Ok(Token::String(String::from("DOC more"))),
+ Ok(Token::String(String::from(" than"))),
+ Ok(Token::String(String::from(" one"))),
+ Ok(Token::String(String::from(" line."))),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
+ Ok(Token::String(String::from("docu"))),
+ Ok(Token::String(String::from("first.paragraph"))),
+ Ok(Token::String(String::from("isn't parsed as tokens"))),
+ Ok(Token::String(String::from(""))),
+ Ok(Token::String(String::from("second paragraph."))),
+ Ok(Token::End),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_file_label() {
+ check_scan(
+ r#"FIL label isn't quoted.
+FILE
+ lab 'is quoted'.
+FILE /*
+/**/ lab not quoted here either
+
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("FIL").unwrap())),
+ Ok(Token::Id(Identifier::new("label").unwrap())),
+ Ok(Token::String(String::from("isn't quoted"))),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("FILE").unwrap())),
+ Ok(Token::Id(Identifier::new("lab").unwrap())),
+ Ok(Token::String(String::from("is quoted"))),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("FILE").unwrap())),
+ Ok(Token::Id(Identifier::new("lab").unwrap())),
+ Ok(Token::String(String::from("not quoted here either"))),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_begin_data() {
+ check_scan(
+ r#"begin data.
+123
+xxx
+end data.
+
+BEG /**/ DAT /*
+5 6 7 /* x
+
+end data
+end data
+.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("begin").unwrap())),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::End),
+ Ok(Token::String(String::from("123"))),
+ Ok(Token::String(String::from("xxx"))),
+ Ok(Token::Id(Identifier::new("end").unwrap())),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::End),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("BEG").unwrap())),
+ Ok(Token::Id(Identifier::new("DAT").unwrap())),
+ Ok(Token::String(String::from("5 6 7 /* x"))),
+ Ok(Token::String(String::from(""))),
+ Ok(Token::String(String::from("end data"))),
+ Ok(Token::Id(Identifier::new("end").unwrap())),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_do_repeat() {
+ check_scan(
+ r#"do repeat x=a b c
+ y=d e f.
+ do repeat a=1 thru 5.
+another command.
+second command
++ third command.
+end /* x */ /* y */ repeat print.
+end
+ repeat.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("do").unwrap())),
+ Ok(Token::Id(Identifier::new("repeat").unwrap())),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Id(Identifier::new("a").unwrap())),
+ Ok(Token::Id(Identifier::new("b").unwrap())),
+ Ok(Token::Id(Identifier::new("c").unwrap())),
+ Ok(Token::Id(Identifier::new("y").unwrap())),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Id(Identifier::new("d").unwrap())),
+ Ok(Token::Id(Identifier::new("e").unwrap())),
+ Ok(Token::Id(Identifier::new("f").unwrap())),
+ Ok(Token::End),
+ Ok(Token::String(String::from(" do repeat a=1 thru 5."))),
+ Ok(Token::String(String::from("another command."))),
+ Ok(Token::String(String::from("second command"))),
+ Ok(Token::String(String::from("+ third command."))),
+ Ok(Token::String(String::from(
+ "end /* x */ /* y */ repeat print.",
+ ))),
+ Ok(Token::Id(Identifier::new("end").unwrap())),
+ Ok(Token::Id(Identifier::new("repeat").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+}
+
+#[test]
+fn test_do_repeat_batch() {
+ check_scan(
+ r#"do repeat x=a b c
+ y=d e f
+do repeat a=1 thru 5
+another command
+second command
++ third command
+end /* x */ /* y */ repeat print
+end
+ repeat
+do
+ repeat #a=1
+
+ inner command
+end repeat
+"#,
+ Syntax::Batch,
+ &[
+ Ok(Token::Id(Identifier::new("do").unwrap())),
+ Ok(Token::Id(Identifier::new("repeat").unwrap())),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Id(Identifier::new("a").unwrap())),
+ Ok(Token::Id(Identifier::new("b").unwrap())),
+ Ok(Token::Id(Identifier::new("c").unwrap())),
+ Ok(Token::Id(Identifier::new("y").unwrap())),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Id(Identifier::new("d").unwrap())),
+ Ok(Token::Id(Identifier::new("e").unwrap())),
+ Ok(Token::Id(Identifier::new("f").unwrap())),
+ Ok(Token::End),
+ Ok(Token::String(String::from("do repeat a=1 thru 5"))),
+ Ok(Token::String(String::from("another command"))),
+ Ok(Token::String(String::from("second command"))),
+ Ok(Token::String(String::from("+ third command"))),
+ Ok(Token::String(String::from(
+ "end /* x */ /* y */ repeat print",
+ ))),
+ Ok(Token::Id(Identifier::new("end").unwrap())),
+ Ok(Token::Id(Identifier::new("repeat").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("do").unwrap())),
+ Ok(Token::Id(Identifier::new("repeat").unwrap())),
+ Ok(Token::Id(Identifier::new("#a").unwrap())),
+ Ok(Token::Punct(Punct::Equals)),
+ Ok(Token::Number(1.0)),
+ Ok(Token::End),
+ Ok(Token::String(String::from(" inner command"))),
+ Ok(Token::Id(Identifier::new("end").unwrap())),
+ Ok(Token::Id(Identifier::new("repeat").unwrap())),
+ ],
+ );
+}
+
+#[test]
+fn test_batch_mode() {
+ check_scan(
+ r#"first command
+ another line of first command
++ second command
+third command
+
+fourth command.
+ fifth command.
+"#,
+ Syntax::Batch,
+ &[
+ Ok(Token::Id(Identifier::new("first").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::Id(Identifier::new("another").unwrap())),
+ Ok(Token::Id(Identifier::new("line").unwrap())),
+ Ok(Token::Id(Identifier::new("of").unwrap())),
+ Ok(Token::Id(Identifier::new("first").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("second").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("third").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("fourth").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("fifth").unwrap())),
+ Ok(Token::Id(Identifier::new("command").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+}
+
+mod define {
+ use crate::{
+ identifier::Identifier,
+ lex::{
+ segment::Syntax,
+ token::{Punct, Token},
+ },
+ };
+
+ use super::check_scan;
+
+ #[test]
+ fn test_simple() {
+ check_scan(
+ r#"define !macro1()
+var1 var2 var3
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from("var1 var2 var3"))),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_no_newline_after_parentheses() {
+ check_scan(
+ r#"define !macro1() var1 var2 var3
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from(" var1 var2 var3"))),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_no_newline_before_enddefine() {
+ check_scan(
+ r#"define !macro1()
+var1 var2 var3!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from("var1 var2 var3"))),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_all_on_one_line() {
+ check_scan(
+ r#"define !macro1()var1 var2 var3!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from("var1 var2 var3"))),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_empty() {
+ check_scan(
+ r#"define !macro1()
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_blank_lines() {
+ check_scan(
+ r#"define !macro1()
+
+
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from(""))),
+ Ok(Token::String(String::from(""))),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_arguments() {
+ check_scan(
+ r#"define !macro1(a(), b(), c())
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Id(Identifier::new("a").unwrap())),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Id(Identifier::new("b").unwrap())),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Id(Identifier::new("c").unwrap())),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_multiline_arguments() {
+ check_scan(
+ r#"define !macro1(
+ a(), b(
+ ),
+ c()
+)
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Id(Identifier::new("a").unwrap())),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Id(Identifier::new("b").unwrap())),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Id(Identifier::new("c").unwrap())),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_arguments_start_on_second_line() {
+ check_scan(
+ r#"define !macro1
+(x,y,z
+)
+content 1
+content 2
+!enddefine.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Id(Identifier::new("y").unwrap())),
+ Ok(Token::Punct(Punct::Comma)),
+ Ok(Token::Id(Identifier::new("z").unwrap())),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from("content 1"))),
+ Ok(Token::String(String::from("content 2"))),
+ Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_1() {
+ check_scan(
+ r#"define !macro1.
+data list /x 1.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::Id(Identifier::new("list").unwrap())),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Number(1.0)),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_2() {
+ check_scan(
+ r#"define !macro1
+x.
+data list /x 1.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::Id(Identifier::new("list").unwrap())),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Number(1.0)),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_3() {
+ check_scan(
+ r#"define !macro1(.
+x.
+data list /x 1.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::Id(Identifier::new("list").unwrap())),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Number(1.0)),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_4() {
+ // Notice the command terminator at the end of the DEFINE command,
+ // which should not be there and ends it early.
+ check_scan(
+ r#"define !macro1.
+data list /x 1.
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::End),
+ Ok(Token::Id(Identifier::new("data").unwrap())),
+ Ok(Token::Id(Identifier::new("list").unwrap())),
+ Ok(Token::Punct(Punct::Slash)),
+ Ok(Token::Id(Identifier::new("x").unwrap())),
+ Ok(Token::Number(1.0)),
+ Ok(Token::End),
+ ],
+ );
+ }
+
+ #[test]
+ fn test_missing_enddefine() {
+ check_scan(
+ r#"define !macro1()
+content line 1
+content line 2
+"#,
+ Syntax::Auto,
+ &[
+ Ok(Token::Id(Identifier::new("define").unwrap())),
+ Ok(Token::String(String::from("!macro1"))),
+ Ok(Token::Punct(Punct::LParen)),
+ Ok(Token::Punct(Punct::RParen)),
+ Ok(Token::String(String::from("content line 1"))),
+ Ok(Token::String(String::from("content line 2"))),
+ ],
+ );
+ }
+}
}
#[cfg(test)]
-mod test;
+mod tests;
+++ /dev/null
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program. If not, see <http://www.gnu.org/licenses/>.
-
-use crate::prompt::PromptStyle;
-
-use super::{Segment, Segmenter, Syntax};
-
-fn push_segment(
- segmenter: &mut Segmenter,
- input: &str,
- one_byte: bool,
-) -> Option<(usize, Segment)> {
- if one_byte {
- for len in input.char_indices().map(|(pos, _c)| pos) {
- if let Ok(result) = segmenter.push(&input[..len], false) {
- return result;
- }
- }
- }
- segmenter.push(input, true).unwrap()
-}
-
-fn _check_segmentation(
- mut input: &str,
- mode: Syntax,
- expect_segments: &[(Segment, &str)],
- expect_prompts: &[PromptStyle],
- one_byte: bool,
-) {
- let mut segments = Vec::with_capacity(expect_segments.len());
- let mut prompts = Vec::new();
- let mut segmenter = Segmenter::new(mode, false);
- while let Some((seg_len, seg_type)) = push_segment(&mut segmenter, input, one_byte) {
- let (token, rest) = input.split_at(seg_len);
- segments.push((seg_type, token));
- if let Segment::Newline = seg_type {
- prompts.push(segmenter.prompt());
- }
- input = rest;
- }
-
- if segments != expect_segments {
- eprintln!("segments differ from expected:");
- let difference = diff::slice(expect_segments, &segments);
- for result in difference {
- match result {
- diff::Result::Left(left) => eprintln!("-{left:?}"),
- diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
- diff::Result::Right(right) => eprintln!("+{right:?}"),
- }
- }
- panic!();
- }
-
- if prompts != expect_prompts {
- eprintln!("prompts differ from expected:");
- let difference = diff::slice(expect_prompts, &prompts);
- for result in difference {
- match result {
- diff::Result::Left(left) => eprintln!("-{left:?}"),
- diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
- diff::Result::Right(right) => eprintln!("+{right:?}"),
- }
- }
- panic!();
- }
-}
-
-fn check_segmentation(
- input: &str,
- mode: Syntax,
- expect_segments: &[(Segment, &str)],
- expect_prompts: &[PromptStyle],
-) {
- for (one_byte, one_byte_name) in [(false, "full-string"), (true, "byte-by-byte")] {
- println!("running {one_byte_name} segmentation test with LF newlines...");
- _check_segmentation(input, mode, expect_segments, expect_prompts, one_byte);
-
- println!("running {one_byte_name} segmentation test with CRLF newlines...");
- _check_segmentation(
- &input.replace('\n', "\r\n"),
- mode,
- &expect_segments
- .iter()
- .map(|(segment, s)| match *segment {
- Segment::Newline => (Segment::Newline, "\r\n"),
- _ => (*segment, *s),
- })
- .collect::<Vec<_>>(),
- expect_prompts,
- one_byte,
- );
-
- if let Some(input) = input.strip_suffix('\n') {
- println!("running {one_byte_name} segmentation test without final newline...");
- let mut expect_segments: Vec<_> = expect_segments.to_vec();
- assert_eq!(expect_segments.pop(), Some((Segment::Newline, "\n")));
- while let Some((Segment::SeparateCommands | Segment::EndCommand, "")) =
- expect_segments.last()
- {
- expect_segments.pop();
- }
- _check_segmentation(
- input,
- mode,
- &expect_segments,
- &expect_prompts[..expect_prompts.len() - 1],
- one_byte,
- );
- }
- }
-}
-
-#[allow(dead_code)]
-fn print_segmentation(mut input: &str) {
- let mut segmenter = Segmenter::new(Syntax::Interactive, false);
- while let Some((seg_len, seg_type)) = segmenter.push(input, true).unwrap() {
- let (token, rest) = input.split_at(seg_len);
- print!("{seg_type:?} {token:?}");
- if let Segment::Newline = seg_type {
- print!(" ({:?})", segmenter.prompt())
- }
- println!();
- input = rest;
- }
-}
-
-#[test]
-fn test_identifiers() {
- check_segmentation(
- r#"a ab abc abcd !abcd
-A AB ABC ABCD !ABCD
-aB aBC aBcD !aBcD
-$x $y $z !$z
-grève Ângstrom poté
-#a #b #c ## #d !#d
-@efg @ @@. @#@ !@
-## # #12345 #.#
-f@#_.#6
-GhIjK
-.x 1y _z
-!abc abc!
-"#,
- Syntax::Auto,
- &[
- (Segment::Identifier, "a"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ab"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "abc"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "abcd"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "!abcd"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "A"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "AB"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ABC"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ABCD"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "!ABCD"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "aB"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "aBC"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "aBcD"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "!aBcD"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "$x"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "$y"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "$z"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "!$z"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "grève"),
- (Segment::Spaces, "\u{00a0}"),
- (Segment::Identifier, "Ângstrom"),
- (Segment::Spaces, "\u{00a0}"),
- (Segment::Identifier, "poté"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "#a"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#b"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#c"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "##"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#d"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "!#d"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "@efg"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "@"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "@@."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "@#@"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "!@"),
- (Segment::Spaces, " "),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "##"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#12345"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#.#"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "f@#_.#6"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "GhIjK"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Identifier, "x"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::Identifier, "y"),
- (Segment::Spaces, " "),
- (Segment::Punct, "_"),
- (Segment::Identifier, "z"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!abc"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "abc"),
- (Segment::Punct, "!"),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- ],
- );
-}
-
-#[test]
-fn test_identifiers_ending_in_dot() {
- check_segmentation(
- r#"abcd. abcd.
-ABCD. ABCD.
-aBcD. aBcD.
-$y. $z. あいうえお.
-#c. #d..
-@@. @@....
-#.#.
-#abcd.
-.
-.
-LMNOP.
-QRSTUV./* end of line comment */
-qrstuv. /* end of line comment */
-QrStUv./* end of line comment */
-wxyz./* unterminated end of line comment
-WXYZ. /* unterminated end of line comment
-WxYz./* unterminated end of line comment
-"#,
- Syntax::Auto,
- &[
- (Segment::Identifier, "abcd."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "abcd"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "ABCD."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ABCD"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "aBcD."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "aBcD"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "$y."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "$z."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "あいうえお"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "#c."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#d."),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "@@."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "@@..."),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "#.#"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "#abcd"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "LMNOP"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "QRSTUV"),
- (Segment::EndCommand, "."),
- (Segment::Comment, "/* end of line comment */"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "qrstuv"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* end of line comment */"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "QrStUv"),
- (Segment::EndCommand, "."),
- (Segment::Comment, "/* end of line comment */"),
- (Segment::Spaces, " "),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "wxyz"),
- (Segment::EndCommand, "."),
- (Segment::Comment, "/* unterminated end of line comment"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "WXYZ"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* unterminated end of line comment"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "WxYz"),
- (Segment::EndCommand, "."),
- (Segment::Comment, "/* unterminated end of line comment "),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_reserved_words() {
- check_segmentation(
- r#"and or not eq ge gt le lt ne all by to with
-AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
-andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
-and. with.
-"#,
- Syntax::Auto,
- &[
- (Segment::Identifier, "and"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "or"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "not"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "eq"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ge"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "gt"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "le"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "lt"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ne"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "all"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "by"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "to"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "with"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "AND"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "OR"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "NOT"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "EQ"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "GE"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "GT"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "LE"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "LT"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "NE"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ALL"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "BY"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "TO"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "WITH"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "andx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "orx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "notx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "eqx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "gex"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "gtx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "lex"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ltx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "nex"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "allx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "byx"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "tox"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "withx"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "and."),
- (Segment::Spaces, " "),
- (Segment::Identifier, "with"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_punctuation() {
- check_segmentation(
- r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
-~&|=>=><=<~=<>(),-+*/[]**!*
-% : ; ? _ ` { } ~ !*
-"#,
- Syntax::Auto,
- &[
- (Segment::Punct, "~"),
- (Segment::Spaces, " "),
- (Segment::Punct, "&"),
- (Segment::Spaces, " "),
- (Segment::Punct, "|"),
- (Segment::Spaces, " "),
- (Segment::Punct, "="),
- (Segment::Spaces, " "),
- (Segment::Punct, ">="),
- (Segment::Spaces, " "),
- (Segment::Punct, ">"),
- (Segment::Spaces, " "),
- (Segment::Punct, "<="),
- (Segment::Spaces, " "),
- (Segment::Punct, "<"),
- (Segment::Spaces, " "),
- (Segment::Punct, "~="),
- (Segment::Spaces, " "),
- (Segment::Punct, "<>"),
- (Segment::Spaces, " "),
- (Segment::Punct, "("),
- (Segment::Spaces, " "),
- (Segment::Punct, ")"),
- (Segment::Spaces, " "),
- (Segment::Punct, ","),
- (Segment::Spaces, " "),
- (Segment::Punct, "-"),
- (Segment::Spaces, " "),
- (Segment::Punct, "+"),
- (Segment::Spaces, " "),
- (Segment::Punct, "*"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Spaces, " "),
- (Segment::Punct, "["),
- (Segment::Spaces, " "),
- (Segment::Punct, "]"),
- (Segment::Spaces, " "),
- (Segment::Punct, "**"),
- (Segment::Newline, "\n"),
- (Segment::Punct, "~"),
- (Segment::Punct, "&"),
- (Segment::Punct, "|"),
- (Segment::Punct, "="),
- (Segment::Punct, ">="),
- (Segment::Punct, ">"),
- (Segment::Punct, "<="),
- (Segment::Punct, "<"),
- (Segment::Punct, "~="),
- (Segment::Punct, "<>"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Punct, ","),
- (Segment::Punct, "-"),
- (Segment::Punct, "+"),
- (Segment::Punct, "*"),
- (Segment::Punct, "/"),
- (Segment::Punct, "["),
- (Segment::Punct, "]"),
- (Segment::Punct, "**"),
- (Segment::Punct, "!*"),
- (Segment::Newline, "\n"),
- (Segment::Punct, "%"),
- (Segment::Spaces, " "),
- (Segment::Punct, ":"),
- (Segment::Spaces, " "),
- (Segment::Punct, ";"),
- (Segment::Spaces, " "),
- (Segment::Punct, "?"),
- (Segment::Spaces, " "),
- (Segment::Punct, "_"),
- (Segment::Spaces, " "),
- (Segment::Punct, "`"),
- (Segment::Spaces, " "),
- (Segment::Punct, "{"),
- (Segment::Spaces, " "),
- (Segment::Punct, "}"),
- (Segment::Spaces, " "),
- (Segment::Punct, "~"),
- (Segment::Spaces, " "),
- (Segment::Punct, "!*"),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Later, PromptStyle::Later, PromptStyle::Later],
- );
-}
-
-#[test]
-fn test_positive_numbers() {
- check_segmentation(
- r#"0 1 01 001. 1.
-123. /* comment 1 */ /* comment 2 */
-.1 0.1 00.1 00.10
-5e1 6E-1 7e+1 6E+01 6e-03
-.3E1 .4e-1 .5E+1 .6e+01 .7E-03
-1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
-. 1e e1 1e+ 1e- 1.
-"#,
- Syntax::Auto,
- &[
- (Segment::Number, "0"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::Spaces, " "),
- (Segment::Number, "01"),
- (Segment::Spaces, " "),
- (Segment::Number, "001."),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Number, "123"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* comment 1 */"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* comment 2 */"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Number, "1"),
- (Segment::Spaces, " "),
- (Segment::Number, "0.1"),
- (Segment::Spaces, " "),
- (Segment::Number, "00.1"),
- (Segment::Spaces, " "),
- (Segment::Number, "00.10"),
- (Segment::Newline, "\n"),
- (Segment::Number, "5e1"),
- (Segment::Spaces, " "),
- (Segment::Number, "6E-1"),
- (Segment::Spaces, " "),
- (Segment::Number, "7e+1"),
- (Segment::Spaces, " "),
- (Segment::Number, "6E+01"),
- (Segment::Spaces, " "),
- (Segment::Number, "6e-03"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Number, "3E1"),
- (Segment::Spaces, " "),
- (Segment::Number, ".4e-1"),
- (Segment::Spaces, " "),
- (Segment::Number, ".5E+1"),
- (Segment::Spaces, " "),
- (Segment::Number, ".6e+01"),
- (Segment::Spaces, " "),
- (Segment::Number, ".7E-03"),
- (Segment::Newline, "\n"),
- (Segment::Number, "1.23e1"),
- (Segment::Spaces, " "),
- (Segment::Number, "45.6E-1"),
- (Segment::Spaces, " "),
- (Segment::Number, "78.9e+1"),
- (Segment::Spaces, " "),
- (Segment::Number, "99.9E+01"),
- (Segment::Spaces, " "),
- (Segment::Number, "11.2e-03"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Spaces, " "),
- (Segment::ExpectedExponent, "1e"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "e1"),
- (Segment::Spaces, " "),
- (Segment::ExpectedExponent, "1e+"),
- (Segment::Spaces, " "),
- (Segment::ExpectedExponent, "1e-"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_negative_numbers() {
- check_segmentation(
- r#" -0 -1 -01 -001. -1.
- -123. /* comment 1 */ /* comment 2 */
- -.1 -0.1 -00.1 -00.10
- -5e1 -6E-1 -7e+1 -6E+01 -6e-03
- -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
- -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
- -/**/1
- -. -1e -e1 -1e+ -1e- -1.
-"#,
- Syntax::Auto,
- &[
- (Segment::Spaces, " "),
- (Segment::Number, "-0"),
- (Segment::Spaces, " "),
- (Segment::Number, "-1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-01"),
- (Segment::Spaces, " "),
- (Segment::Number, "-001."),
- (Segment::Spaces, " "),
- (Segment::Number, "-1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Number, "-123"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* comment 1 */"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* comment 2 */"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Number, "-.1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-0.1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-00.1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-00.10"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Number, "-5e1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-6E-1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-7e+1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-6E+01"),
- (Segment::Spaces, " "),
- (Segment::Number, "-6e-03"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Number, "-.3E1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-.4e-1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-.5E+1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-.6e+01"),
- (Segment::Spaces, " "),
- (Segment::Number, "-.7E-03"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Number, "-1.23e1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-45.6E-1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-78.9e+1"),
- (Segment::Spaces, " "),
- (Segment::Number, "-99.9E+01"),
- (Segment::Spaces, " "),
- (Segment::Number, "-11.2e-03"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Punct, "-"),
- (Segment::Comment, "/**/"),
- (Segment::Number, "1"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Punct, "-"),
- (Segment::Punct, "."),
- (Segment::Spaces, " "),
- (Segment::ExpectedExponent, "-1e"),
- (Segment::Spaces, " "),
- (Segment::Punct, "-"),
- (Segment::Identifier, "e1"),
- (Segment::Spaces, " "),
- (Segment::ExpectedExponent, "-1e+"),
- (Segment::Spaces, " "),
- (Segment::ExpectedExponent, "-1e-"),
- (Segment::Spaces, " "),
- (Segment::Number, "-1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_strings() {
- check_segmentation(
- r#"'x' "y" 'abc'
-'Don''t' "Can't" 'Won''t'
-"""quoted""" '"quoted"'
-'' ""
-'missing end quote
-"missing double quote
-x"4142" X'5152'
-u'fffd' U"041"
-+ new command
-+ /* comment */ 'string continuation'
-+ /* also a punctuator on blank line
-- 'new command'
-"#,
- Syntax::Auto,
- &[
- (Segment::QuotedString, "'x'"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "\"y\""),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "'abc'"),
- (Segment::Newline, "\n"),
- (Segment::QuotedString, "'Don''t'"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "\"Can't\""),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "'Won''t'"),
- (Segment::Newline, "\n"),
- (Segment::QuotedString, "\"\"\"quoted\"\"\""),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "'\"quoted\"'"),
- (Segment::Newline, "\n"),
- (Segment::QuotedString, "''"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "\"\""),
- (Segment::Newline, "\n"),
- (Segment::ExpectedQuote, "'missing end quote"),
- (Segment::Newline, "\n"),
- (Segment::ExpectedQuote, "\"missing double quote"),
- (Segment::Newline, "\n"),
- (Segment::HexString, "x\"4142\""),
- (Segment::Spaces, " "),
- (Segment::HexString, "X'5152'"),
- (Segment::Newline, "\n"),
- (Segment::UnicodeString, "u'fffd'"),
- (Segment::Spaces, " "),
- (Segment::UnicodeString, "U\"041\""),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "+"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "new"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::Punct, "+"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* comment */"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "'string continuation'"),
- (Segment::Newline, "\n"),
- (Segment::Punct, "+"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/* also a punctuator on blank line"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "-"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "'new command'"),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- ],
- );
-}
-
-#[test]
-fn test_shbang() {
- check_segmentation(
- r#"#! /usr/bin/pspp
-title my title.
-#! /usr/bin/pspp
-"#,
- Syntax::Interactive,
- &[
- (Segment::Shbang, "#! /usr/bin/pspp"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "title"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "my"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "title"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "#"),
- (Segment::Punct, "!"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Identifier, "usr"),
- (Segment::Punct, "/"),
- (Segment::Identifier, "bin"),
- (Segment::Punct, "/"),
- (Segment::Identifier, "pspp"),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::First, PromptStyle::First, PromptStyle::Later],
- );
-}
-
-#[test]
-fn test_comment_command() {
- check_segmentation(
- r#"* Comment commands "don't
-have to contain valid tokens.
-
-** Check ambiguity with ** token.
-****************.
-
-comment keyword works too.
-COMM also.
-com is ambiguous with COMPUTE.
-
- * Comment need not start at left margin.
-
-* Comment ends with blank line
-
-next command.
-
-"#,
- Syntax::Interactive,
- &[
- (Segment::CommentCommand, "* Comment commands \"don't"),
- (Segment::Newline, "\n"),
- (Segment::CommentCommand, "have to contain valid tokens"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::CommentCommand, "** Check ambiguity with ** token"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::CommentCommand, "****************"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::CommentCommand, "comment keyword works too"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::CommentCommand, "COMM also"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "com"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "is"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "ambiguous"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "with"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "COMPUTE"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (
- Segment::CommentCommand,
- "* Comment need not start at left margin",
- ),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::CommentCommand, "* Comment ends with blank line"),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "next"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Comment,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Comment,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_document_command() {
- check_segmentation(
- r#"DOCUMENT one line.
-DOC more
- than
- one
- line.
-docu
-first.paragraph
-isn't parsed as tokens
-
-second paragraph.
-"#,
- Syntax::Interactive,
- &[
- (Segment::StartDocument, ""),
- (Segment::Document, "DOCUMENT one line."),
- (Segment::EndCommand, ""),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::StartDocument, ""),
- (Segment::Document, "DOC more"),
- (Segment::Newline, "\n"),
- (Segment::Document, " than"),
- (Segment::Newline, "\n"),
- (Segment::Document, " one"),
- (Segment::Newline, "\n"),
- (Segment::Document, " line."),
- (Segment::EndCommand, ""),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::StartDocument, ""),
- (Segment::Document, "docu"),
- (Segment::Newline, "\n"),
- (Segment::Document, "first.paragraph"),
- (Segment::Newline, "\n"),
- (Segment::Document, "isn't parsed as tokens"),
- (Segment::Newline, "\n"),
- (Segment::Document, ""),
- (Segment::Newline, "\n"),
- (Segment::Document, "second paragraph."),
- (Segment::EndCommand, ""),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::First,
- PromptStyle::Document,
- PromptStyle::Document,
- PromptStyle::Document,
- PromptStyle::First,
- PromptStyle::Document,
- PromptStyle::Document,
- PromptStyle::Document,
- PromptStyle::Document,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_file_label_command() {
- check_segmentation(
- r#"FIL label isn't quoted.
-FILE
- lab 'is quoted'.
-FILE /*
-/**/ lab not quoted here either
-
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "FIL"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "label"),
- (Segment::Spaces, " "),
- (Segment::UnquotedString, "isn't quoted"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "FILE"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "lab"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "'is quoted'"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "FILE"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/*"),
- (Segment::Newline, "\n"),
- (Segment::Comment, "/**/"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "lab"),
- (Segment::Spaces, " "),
- (Segment::UnquotedString, "not quoted here either"),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_begin_data() {
- check_segmentation(
- r#"begin data.
-end data.
-
-begin data. /*
-123
-xxx
-end data.
-
-BEG /**/ DAT /*
-5 6 7 /* x
-
-end data
-end data
-.
-
-begin
- data.
-data
-end data.
-
-begin data "xxx".
-begin data 123.
-not data
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "begin"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "begin"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Comment, "/*"),
- (Segment::Newline, "\n"),
- (Segment::InlineData, "123"),
- (Segment::Newline, "\n"),
- (Segment::InlineData, "xxx"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "BEG"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/**/"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "DAT"),
- (Segment::Spaces, " "),
- (Segment::Comment, "/*"),
- (Segment::Newline, "\n"),
- (Segment::InlineData, "5 6 7 /* x"),
- (Segment::Newline, "\n"),
- (Segment::InlineData, ""),
- (Segment::Newline, "\n"),
- (Segment::InlineData, "end data"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "begin"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::InlineData, "data"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "begin"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::QuotedString, "\"xxx\""),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "begin"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::Number, "123"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "not"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "data"),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Data,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Data,
- PromptStyle::Data,
- PromptStyle::Data,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Data,
- PromptStyle::Data,
- PromptStyle::Data,
- PromptStyle::Data,
- PromptStyle::Later,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::Data,
- PromptStyle::Data,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Later,
- ],
- );
-}
-
-#[test]
-fn test_do_repeat() {
- check_segmentation(
- r#"do repeat x=a b c
- y=d e f.
- do repeat a=1 thru 5.
-another command.
-second command
-+ third command.
-end /* x */ /* y */ repeat print.
-end
- repeat.
-do
- repeat #a=1.
- inner command.
-end repeat.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "do"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "x"),
- (Segment::Punct, "="),
- (Segment::Identifier, "a"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "b"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "c"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "y"),
- (Segment::Punct, "="),
- (Segment::Identifier, "d"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "e"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "f"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, " do repeat a=1 thru 5."),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "another command."),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "second command"),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "+ third command."),
- (Segment::Newline, "\n"),
- (
- Segment::DoRepeatCommand,
- "end /* x */ /* y */ repeat print.",
- ),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "do"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#a"),
- (Segment::Punct, "="),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, " inner command."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::Later,
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_do_repeat_overflow() {
- const N: usize = 257;
- let do_repeat: Vec<String> = (0..N)
- .map(|i| format!("do repeat v{i}={i} thru {}.\n", i + 5))
- .collect();
- let end_repeat: Vec<String> = (0..N)
- .rev()
- .map(|i| format!("end repeat. /* {i}\n"))
- .collect();
-
- let s: String = do_repeat
- .iter()
- .chain(end_repeat.iter())
- .map(|s| s.as_str())
- .collect();
- let mut expect_output = vec![
- (Segment::Identifier, "do"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "v0"),
- (Segment::Punct, "="),
- (Segment::Number, "0"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "thru"),
- (Segment::Spaces, " "),
- (Segment::Number, "5"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ];
- for (i, line) in do_repeat.iter().enumerate().take(N).skip(1) {
- expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
- if i >= 255 {
- expect_output.push((Segment::DoRepeatOverflow, ""));
- }
- expect_output.push((Segment::Newline, "\n"));
- }
- for line in &end_repeat[..254] {
- expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
- expect_output.push((Segment::Newline, "\n"));
- }
- let comments: Vec<String> = (0..(N - 254)).rev().map(|i| format!("/* {i}")).collect();
- for comment in &comments {
- expect_output.extend([
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::EndCommand, "."),
- (Segment::Spaces, " "),
- (Segment::Comment, comment),
- (Segment::Newline, "\n"),
- ]);
- }
-
- let expect_prompts: Vec<_> = (0..N * 2 - 3)
- .map(|_| PromptStyle::DoRepeat)
- .chain([PromptStyle::First, PromptStyle::First, PromptStyle::First])
- .collect();
- check_segmentation(&s, Syntax::Interactive, &expect_output, &expect_prompts);
-}
-
-#[test]
-fn test_do_repeat_batch() {
- check_segmentation(
- r#"do repeat x=a b c
- y=d e f
-do repeat a=1 thru 5
-another command
-second command
-+ third command
-end /* x */ /* y */ repeat print
-end
- repeat
-do
- repeat #a=1
-
- inner command
-end repeat
-"#,
- Syntax::Batch,
- &[
- (Segment::Identifier, "do"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "x"),
- (Segment::Punct, "="),
- (Segment::Identifier, "a"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "b"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "c"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "y"),
- (Segment::Punct, "="),
- (Segment::Identifier, "d"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "e"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "f"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, ""),
- (Segment::DoRepeatCommand, "do repeat a=1 thru 5"),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "another command"),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "second command"),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "+ third command"),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, "end /* x */ /* y */ repeat print"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, ""),
- (Segment::Identifier, "do"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "#a"),
- (Segment::Punct, "="),
- (Segment::Number, "1"),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::DoRepeatCommand, " inner command"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "end"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "repeat"),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::DoRepeat,
- PromptStyle::DoRepeat,
- PromptStyle::Later,
- ],
- );
-}
-
-mod define {
- use crate::{
- lex::segment::{Segment, Syntax},
- prompt::PromptStyle,
- };
-
- use super::check_segmentation;
-
- #[test]
- fn test_simple() {
- check_segmentation(
- r#"define !macro1()
-var1 var2 var3 "!enddefine"
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, "var1 var2 var3 \"!enddefine\""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Define, PromptStyle::Define, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_no_newline_after_parentheses() {
- check_segmentation(
- r#"define !macro1() var1 var2 var3 /* !enddefine
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::MacroBody, " var1 var2 var3 /* !enddefine"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Define, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_no_newline_before_enddefine() {
- check_segmentation(
- r#"define !macro1()
-var1 var2 var3!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, "var1 var2 var3"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Define, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_all_on_one_line() {
- check_segmentation(
- r#"define !macro1()var1 var2 var3!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::MacroBody, "var1 var2 var3"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::First],
- );
- }
-
- #[test]
- fn test_empty() {
- check_segmentation(
- r#"define !macro1()
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Define, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_blank_lines() {
- check_segmentation(
- r#"define !macro1()
-
-
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, ""),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Define,
- PromptStyle::Define,
- PromptStyle::Define,
- PromptStyle::First,
- ],
- );
- }
-
- #[test]
- fn test_arguments() {
- check_segmentation(
- r#"define !macro1(a(), b(), c())
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Identifier, "a"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Punct, ","),
- (Segment::Spaces, " "),
- (Segment::Identifier, "b"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Punct, ","),
- (Segment::Spaces, " "),
- (Segment::Identifier, "c"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Define, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_multiline_arguments() {
- check_segmentation(
- r#"define !macro1(
- a(), b(
- ),
- c()
-)
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "a"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Punct, ","),
- (Segment::Spaces, " "),
- (Segment::Identifier, "b"),
- (Segment::Punct, "("),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Punct, ")"),
- (Segment::Punct, ","),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "c"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Define,
- PromptStyle::First,
- ],
- );
- }
-
- #[test]
- fn test_arguments_start_on_second_line() {
- check_segmentation(
- r#"define !macro1
-(x,y,z
-)
-content 1
-content 2
-!enddefine.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Newline, "\n"),
- (Segment::Punct, "("),
- (Segment::Identifier, "x"),
- (Segment::Punct, ","),
- (Segment::Identifier, "y"),
- (Segment::Punct, ","),
- (Segment::Identifier, "z"),
- (Segment::Newline, "\n"),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, "content 1"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, "content 2"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "!enddefine"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Define,
- PromptStyle::Define,
- PromptStyle::Define,
- PromptStyle::First,
- ],
- );
- }
-
- #[test]
- fn test_early_end_of_command_1() {
- check_segmentation(
- r#"define !macro1.
-data list /x 1.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "list"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Identifier, "x"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::First, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_early_end_of_command_2() {
- check_segmentation(
- r#"define !macro1
-x.
-data list /x 1.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "x"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "list"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Identifier, "x"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Later, PromptStyle::First, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_early_end_of_command_3() {
- check_segmentation(
- r#"define !macro1(.
-x.
-data list /x 1.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "x"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "list"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Identifier, "x"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::First, PromptStyle::First, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_early_end_of_command_4() {
- // Notice the command terminator at the end of the `DEFINE` command,
- // which should not be there and ends it early.
- check_segmentation(
- r#"define !macro1.
-data list /x 1.
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "list"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Identifier, "x"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::First, PromptStyle::First],
- );
- }
-
- #[test]
- fn test_missing_enddefine() {
- check_segmentation(
- r#"define !macro1()
-content line 1
-content line 2
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, "content line 1"),
- (Segment::Newline, "\n"),
- (Segment::MacroBody, "content line 2"),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Define,
- PromptStyle::Define,
- PromptStyle::Define,
- ],
- );
- }
-
- #[test]
- fn test_missing_enddefine_2() {
- check_segmentation(
- r#"define !macro1()
-"#,
- Syntax::Interactive,
- &[
- (Segment::Identifier, "define"),
- (Segment::Spaces, " "),
- (Segment::MacroName, "!macro1"),
- (Segment::Punct, "("),
- (Segment::Punct, ")"),
- (Segment::Newline, "\n"),
- ],
- &[PromptStyle::Define],
- );
- }
-}
-
-#[test]
-fn test_batch_mode() {
- check_segmentation(
- r#"first command
- another line of first command
-+ second command
-third command
-
-fourth command.
- fifth command.
-"#,
- Syntax::Batch,
- &[
- (Segment::Identifier, "first"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "another"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "line"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "of"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "first"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "+"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "second"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, ""),
- (Segment::Identifier, "third"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "fourth"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "fifth"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- ],
- );
-}
-
-#[test]
-fn test_auto_mode() {
- check_segmentation(
- r#"command
- another line of command
-2sls
-+ another command
-another line of second command
-data list /x 1
-aggregate.
-print eject.
-twostep cluster
-
-
-fourth command.
- fifth command.
-"#,
- Syntax::Auto,
- &[
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "another"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "line"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "of"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, ""),
- (Segment::Number, "2"),
- (Segment::Identifier, "sls"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, "+"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "another"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "another"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "line"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "of"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "second"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, ""),
- (Segment::Identifier, "data"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "list"),
- (Segment::Spaces, " "),
- (Segment::Punct, "/"),
- (Segment::Identifier, "x"),
- (Segment::Spaces, " "),
- (Segment::Number, "1"),
- (Segment::Newline, "\n"),
- (Segment::StartCommand, ""),
- (Segment::Identifier, "aggregate"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "print"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "eject"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "twostep"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "cluster"),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::SeparateCommands, ""),
- (Segment::Newline, "\n"),
- (Segment::Identifier, "fourth"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "fifth"),
- (Segment::Spaces, " "),
- (Segment::Identifier, "command"),
- (Segment::EndCommand, "."),
- (Segment::Newline, "\n"),
- ],
- &[
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::Later,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::Later,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- PromptStyle::First,
- ],
- );
-}
--- /dev/null
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see <http://www.gnu.org/licenses/>.
+
+use crate::prompt::PromptStyle;
+
+use super::{Segment, Segmenter, Syntax};
+
+fn push_segment(
+ segmenter: &mut Segmenter,
+ input: &str,
+ one_byte: bool,
+) -> Option<(usize, Segment)> {
+ if one_byte {
+ for len in input.char_indices().map(|(pos, _c)| pos) {
+ if let Ok(result) = segmenter.push(&input[..len], false) {
+ return result;
+ }
+ }
+ }
+ segmenter.push(input, true).unwrap()
+}
+
+fn _check_segmentation(
+ mut input: &str,
+ mode: Syntax,
+ expect_segments: &[(Segment, &str)],
+ expect_prompts: &[PromptStyle],
+ one_byte: bool,
+) {
+ let mut segments = Vec::with_capacity(expect_segments.len());
+ let mut prompts = Vec::new();
+ let mut segmenter = Segmenter::new(mode, false);
+ while let Some((seg_len, seg_type)) = push_segment(&mut segmenter, input, one_byte) {
+ let (token, rest) = input.split_at(seg_len);
+ segments.push((seg_type, token));
+ if let Segment::Newline = seg_type {
+ prompts.push(segmenter.prompt());
+ }
+ input = rest;
+ }
+
+ if segments != expect_segments {
+ eprintln!("segments differ from expected:");
+ let difference = diff::slice(expect_segments, &segments);
+ for result in difference {
+ match result {
+ diff::Result::Left(left) => eprintln!("-{left:?}"),
+ diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
+ diff::Result::Right(right) => eprintln!("+{right:?}"),
+ }
+ }
+ panic!();
+ }
+
+ if prompts != expect_prompts {
+ eprintln!("prompts differ from expected:");
+ let difference = diff::slice(expect_prompts, &prompts);
+ for result in difference {
+ match result {
+ diff::Result::Left(left) => eprintln!("-{left:?}"),
+ diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
+ diff::Result::Right(right) => eprintln!("+{right:?}"),
+ }
+ }
+ panic!();
+ }
+}
+
+fn check_segmentation(
+ input: &str,
+ mode: Syntax,
+ expect_segments: &[(Segment, &str)],
+ expect_prompts: &[PromptStyle],
+) {
+ for (one_byte, one_byte_name) in [(false, "full-string"), (true, "byte-by-byte")] {
+ println!("running {one_byte_name} segmentation test with LF newlines...");
+ _check_segmentation(input, mode, expect_segments, expect_prompts, one_byte);
+
+ println!("running {one_byte_name} segmentation test with CRLF newlines...");
+ _check_segmentation(
+ &input.replace('\n', "\r\n"),
+ mode,
+ &expect_segments
+ .iter()
+ .map(|(segment, s)| match *segment {
+ Segment::Newline => (Segment::Newline, "\r\n"),
+ _ => (*segment, *s),
+ })
+ .collect::<Vec<_>>(),
+ expect_prompts,
+ one_byte,
+ );
+
+ if let Some(input) = input.strip_suffix('\n') {
+ println!("running {one_byte_name} segmentation test without final newline...");
+ let mut expect_segments: Vec<_> = expect_segments.to_vec();
+ assert_eq!(expect_segments.pop(), Some((Segment::Newline, "\n")));
+ while let Some((Segment::SeparateCommands | Segment::EndCommand, "")) =
+ expect_segments.last()
+ {
+ expect_segments.pop();
+ }
+ _check_segmentation(
+ input,
+ mode,
+ &expect_segments,
+ &expect_prompts[..expect_prompts.len() - 1],
+ one_byte,
+ );
+ }
+ }
+}
+
+#[allow(dead_code)]
+fn print_segmentation(mut input: &str) {
+ let mut segmenter = Segmenter::new(Syntax::Interactive, false);
+ while let Some((seg_len, seg_type)) = segmenter.push(input, true).unwrap() {
+ let (token, rest) = input.split_at(seg_len);
+ print!("{seg_type:?} {token:?}");
+ if let Segment::Newline = seg_type {
+ print!(" ({:?})", segmenter.prompt())
+ }
+ println!();
+ input = rest;
+ }
+}
+
+#[test]
+fn test_identifiers() {
+ check_segmentation(
+ r#"a ab abc abcd !abcd
+A AB ABC ABCD !ABCD
+aB aBC aBcD !aBcD
+$x $y $z !$z
+grève Ângstrom poté
+#a #b #c ## #d !#d
+@efg @ @@. @#@ !@
+## # #12345 #.#
+f@#_.#6
+GhIjK
+.x 1y _z
+!abc abc!
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Identifier, "a"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ab"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "abc"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "abcd"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "!abcd"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "A"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "AB"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ABC"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ABCD"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "!ABCD"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "aB"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "aBC"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "aBcD"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "!aBcD"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "$x"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "$y"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "$z"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "!$z"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "grève"),
+ (Segment::Spaces, "\u{00a0}"),
+ (Segment::Identifier, "Ângstrom"),
+ (Segment::Spaces, "\u{00a0}"),
+ (Segment::Identifier, "poté"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "#a"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#b"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#c"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "##"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#d"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "!#d"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "@efg"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "@"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "@@."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "@#@"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "!@"),
+ (Segment::Spaces, " "),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "##"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#12345"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#.#"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "f@#_.#6"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "GhIjK"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Identifier, "x"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::Identifier, "y"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "_"),
+ (Segment::Identifier, "z"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!abc"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "abc"),
+ (Segment::Punct, "!"),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ ],
+ );
+}
+
+#[test]
+fn test_identifiers_ending_in_dot() {
+ check_segmentation(
+ r#"abcd. abcd.
+ABCD. ABCD.
+aBcD. aBcD.
+$y. $z. あいうえお.
+#c. #d..
+@@. @@....
+#.#.
+#abcd.
+.
+.
+LMNOP.
+QRSTUV./* end of line comment */
+qrstuv. /* end of line comment */
+QrStUv./* end of line comment */
+wxyz./* unterminated end of line comment
+WXYZ. /* unterminated end of line comment
+WxYz./* unterminated end of line comment
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Identifier, "abcd."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "abcd"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "ABCD."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ABCD"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "aBcD."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "aBcD"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "$y."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "$z."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "あいうえお"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "#c."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#d."),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "@@."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "@@..."),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "#.#"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "#abcd"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "LMNOP"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "QRSTUV"),
+ (Segment::EndCommand, "."),
+ (Segment::Comment, "/* end of line comment */"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "qrstuv"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* end of line comment */"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "QrStUv"),
+ (Segment::EndCommand, "."),
+ (Segment::Comment, "/* end of line comment */"),
+ (Segment::Spaces, " "),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "wxyz"),
+ (Segment::EndCommand, "."),
+ (Segment::Comment, "/* unterminated end of line comment"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "WXYZ"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* unterminated end of line comment"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "WxYz"),
+ (Segment::EndCommand, "."),
+ (Segment::Comment, "/* unterminated end of line comment "),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_reserved_words() {
+ check_segmentation(
+ r#"and or not eq ge gt le lt ne all by to with
+AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
+andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
+and. with.
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Identifier, "and"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "or"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "not"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "eq"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ge"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "gt"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "le"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "lt"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ne"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "all"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "by"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "to"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "with"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "AND"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "OR"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "NOT"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "EQ"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "GE"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "GT"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "LE"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "LT"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "NE"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ALL"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "BY"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "TO"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "WITH"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "andx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "orx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "notx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "eqx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "gex"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "gtx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "lex"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ltx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "nex"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "allx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "byx"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "tox"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "withx"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "and."),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "with"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_punctuation() {
+ check_segmentation(
+ r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
+~&|=>=><=<~=<>(),-+*/[]**!*
+% : ; ? _ ` { } ~ !*
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Punct, "~"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "&"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "|"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "="),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ">="),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ">"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "<="),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "<"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "~="),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "<>"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "("),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ")"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ","),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "-"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "+"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "*"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "["),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "]"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "**"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, "~"),
+ (Segment::Punct, "&"),
+ (Segment::Punct, "|"),
+ (Segment::Punct, "="),
+ (Segment::Punct, ">="),
+ (Segment::Punct, ">"),
+ (Segment::Punct, "<="),
+ (Segment::Punct, "<"),
+ (Segment::Punct, "~="),
+ (Segment::Punct, "<>"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Punct, ","),
+ (Segment::Punct, "-"),
+ (Segment::Punct, "+"),
+ (Segment::Punct, "*"),
+ (Segment::Punct, "/"),
+ (Segment::Punct, "["),
+ (Segment::Punct, "]"),
+ (Segment::Punct, "**"),
+ (Segment::Punct, "!*"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, "%"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ":"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ";"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "?"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "_"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "`"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "{"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "}"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "~"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "!*"),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Later, PromptStyle::Later, PromptStyle::Later],
+ );
+}
+
+#[test]
+fn test_positive_numbers() {
+ check_segmentation(
+ r#"0 1 01 001. 1.
+123. /* comment 1 */ /* comment 2 */
+.1 0.1 00.1 00.10
+5e1 6E-1 7e+1 6E+01 6e-03
+.3E1 .4e-1 .5E+1 .6e+01 .7E-03
+1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
+. 1e e1 1e+ 1e- 1.
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Number, "0"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "001."),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Number, "123"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* comment 1 */"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* comment 2 */"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Number, "1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "0.1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "00.1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "00.10"),
+ (Segment::Newline, "\n"),
+ (Segment::Number, "5e1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "6E-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "7e+1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "6E+01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "6e-03"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Number, "3E1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, ".4e-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, ".5E+1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, ".6e+01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, ".7E-03"),
+ (Segment::Newline, "\n"),
+ (Segment::Number, "1.23e1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "45.6E-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "78.9e+1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "99.9E+01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "11.2e-03"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::ExpectedExponent, "1e"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "e1"),
+ (Segment::Spaces, " "),
+ (Segment::ExpectedExponent, "1e+"),
+ (Segment::Spaces, " "),
+ (Segment::ExpectedExponent, "1e-"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_negative_numbers() {
+ check_segmentation(
+ r#" -0 -1 -01 -001. -1.
+ -123. /* comment 1 */ /* comment 2 */
+ -.1 -0.1 -00.1 -00.10
+ -5e1 -6E-1 -7e+1 -6E+01 -6e-03
+ -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
+ -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
+ -/**/1
+ -. -1e -e1 -1e+ -1e- -1.
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Spaces, " "),
+ (Segment::Number, "-0"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-001."),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-123"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* comment 1 */"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* comment 2 */"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-.1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-0.1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-00.1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-00.10"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-5e1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-6E-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-7e+1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-6E+01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-6e-03"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-.3E1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-.4e-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-.5E+1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-.6e+01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-.7E-03"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-1.23e1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-45.6E-1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-78.9e+1"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-99.9E+01"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-11.2e-03"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "-"),
+ (Segment::Comment, "/**/"),
+ (Segment::Number, "1"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "-"),
+ (Segment::Punct, "."),
+ (Segment::Spaces, " "),
+ (Segment::ExpectedExponent, "-1e"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "-"),
+ (Segment::Identifier, "e1"),
+ (Segment::Spaces, " "),
+ (Segment::ExpectedExponent, "-1e+"),
+ (Segment::Spaces, " "),
+ (Segment::ExpectedExponent, "-1e-"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "-1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_strings() {
+ check_segmentation(
+ r#"'x' "y" 'abc'
+'Don''t' "Can't" 'Won''t'
+"""quoted""" '"quoted"'
+'' ""
+'missing end quote
+"missing double quote
+x"4142" X'5152'
+u'fffd' U"041"
++ new command
++ /* comment */ 'string continuation'
++ /* also a punctuator on blank line
+- 'new command'
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::QuotedString, "'x'"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "\"y\""),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "'abc'"),
+ (Segment::Newline, "\n"),
+ (Segment::QuotedString, "'Don''t'"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "\"Can't\""),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "'Won''t'"),
+ (Segment::Newline, "\n"),
+ (Segment::QuotedString, "\"\"\"quoted\"\"\""),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "'\"quoted\"'"),
+ (Segment::Newline, "\n"),
+ (Segment::QuotedString, "''"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "\"\""),
+ (Segment::Newline, "\n"),
+ (Segment::ExpectedQuote, "'missing end quote"),
+ (Segment::Newline, "\n"),
+ (Segment::ExpectedQuote, "\"missing double quote"),
+ (Segment::Newline, "\n"),
+ (Segment::HexString, "x\"4142\""),
+ (Segment::Spaces, " "),
+ (Segment::HexString, "X'5152'"),
+ (Segment::Newline, "\n"),
+ (Segment::UnicodeString, "u'fffd'"),
+ (Segment::Spaces, " "),
+ (Segment::UnicodeString, "U\"041\""),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "+"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "new"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, "+"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* comment */"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "'string continuation'"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, "+"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/* also a punctuator on blank line"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "-"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "'new command'"),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ ],
+ );
+}
+
+#[test]
+fn test_shbang() {
+ check_segmentation(
+ r#"#! /usr/bin/pspp
+title my title.
+#! /usr/bin/pspp
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Shbang, "#! /usr/bin/pspp"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "title"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "my"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "title"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "#"),
+ (Segment::Punct, "!"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "usr"),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "bin"),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "pspp"),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::First, PromptStyle::First, PromptStyle::Later],
+ );
+}
+
+#[test]
+fn test_comment_command() {
+ check_segmentation(
+ r#"* Comment commands "don't
+have to contain valid tokens.
+
+** Check ambiguity with ** token.
+****************.
+
+comment keyword works too.
+COMM also.
+com is ambiguous with COMPUTE.
+
+ * Comment need not start at left margin.
+
+* Comment ends with blank line
+
+next command.
+
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::CommentCommand, "* Comment commands \"don't"),
+ (Segment::Newline, "\n"),
+ (Segment::CommentCommand, "have to contain valid tokens"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::CommentCommand, "** Check ambiguity with ** token"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::CommentCommand, "****************"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::CommentCommand, "comment keyword works too"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::CommentCommand, "COMM also"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "com"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "is"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "ambiguous"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "with"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "COMPUTE"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (
+ Segment::CommentCommand,
+ "* Comment need not start at left margin",
+ ),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::CommentCommand, "* Comment ends with blank line"),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "next"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Comment,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Comment,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_document_command() {
+ check_segmentation(
+ r#"DOCUMENT one line.
+DOC more
+ than
+ one
+ line.
+docu
+first.paragraph
+isn't parsed as tokens
+
+second paragraph.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::StartDocument, ""),
+ (Segment::Document, "DOCUMENT one line."),
+ (Segment::EndCommand, ""),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::StartDocument, ""),
+ (Segment::Document, "DOC more"),
+ (Segment::Newline, "\n"),
+ (Segment::Document, " than"),
+ (Segment::Newline, "\n"),
+ (Segment::Document, " one"),
+ (Segment::Newline, "\n"),
+ (Segment::Document, " line."),
+ (Segment::EndCommand, ""),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::StartDocument, ""),
+ (Segment::Document, "docu"),
+ (Segment::Newline, "\n"),
+ (Segment::Document, "first.paragraph"),
+ (Segment::Newline, "\n"),
+ (Segment::Document, "isn't parsed as tokens"),
+ (Segment::Newline, "\n"),
+ (Segment::Document, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Document, "second paragraph."),
+ (Segment::EndCommand, ""),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::First,
+ PromptStyle::Document,
+ PromptStyle::Document,
+ PromptStyle::Document,
+ PromptStyle::First,
+ PromptStyle::Document,
+ PromptStyle::Document,
+ PromptStyle::Document,
+ PromptStyle::Document,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_file_label_command() {
+ check_segmentation(
+ r#"FIL label isn't quoted.
+FILE
+ lab 'is quoted'.
+FILE /*
+/**/ lab not quoted here either
+
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "FIL"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "label"),
+ (Segment::Spaces, " "),
+ (Segment::UnquotedString, "isn't quoted"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "FILE"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "lab"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "'is quoted'"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "FILE"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/*"),
+ (Segment::Newline, "\n"),
+ (Segment::Comment, "/**/"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "lab"),
+ (Segment::Spaces, " "),
+ (Segment::UnquotedString, "not quoted here either"),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_begin_data() {
+ check_segmentation(
+ r#"begin data.
+end data.
+
+begin data. /*
+123
+xxx
+end data.
+
+BEG /**/ DAT /*
+5 6 7 /* x
+
+end data
+end data
+.
+
+begin
+ data.
+data
+end data.
+
+begin data "xxx".
+begin data 123.
+not data
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "begin"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "begin"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/*"),
+ (Segment::Newline, "\n"),
+ (Segment::InlineData, "123"),
+ (Segment::Newline, "\n"),
+ (Segment::InlineData, "xxx"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "BEG"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/**/"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "DAT"),
+ (Segment::Spaces, " "),
+ (Segment::Comment, "/*"),
+ (Segment::Newline, "\n"),
+ (Segment::InlineData, "5 6 7 /* x"),
+ (Segment::Newline, "\n"),
+ (Segment::InlineData, ""),
+ (Segment::Newline, "\n"),
+ (Segment::InlineData, "end data"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "begin"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::InlineData, "data"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "begin"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::QuotedString, "\"xxx\""),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "begin"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "123"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "not"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "data"),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Data,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Data,
+ PromptStyle::Data,
+ PromptStyle::Data,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Data,
+ PromptStyle::Data,
+ PromptStyle::Data,
+ PromptStyle::Data,
+ PromptStyle::Later,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::Data,
+ PromptStyle::Data,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Later,
+ ],
+ );
+}
+
+#[test]
+fn test_do_repeat() {
+ check_segmentation(
+ r#"do repeat x=a b c
+ y=d e f.
+ do repeat a=1 thru 5.
+another command.
+second command
++ third command.
+end /* x */ /* y */ repeat print.
+end
+ repeat.
+do
+ repeat #a=1.
+ inner command.
+end repeat.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "do"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "x"),
+ (Segment::Punct, "="),
+ (Segment::Identifier, "a"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "b"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "c"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "y"),
+ (Segment::Punct, "="),
+ (Segment::Identifier, "d"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "e"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "f"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, " do repeat a=1 thru 5."),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "another command."),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "second command"),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "+ third command."),
+ (Segment::Newline, "\n"),
+ (
+ Segment::DoRepeatCommand,
+ "end /* x */ /* y */ repeat print.",
+ ),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "do"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#a"),
+ (Segment::Punct, "="),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, " inner command."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::Later,
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_do_repeat_overflow() {
+ const N: usize = 257;
+ let do_repeat: Vec<String> = (0..N)
+ .map(|i| format!("do repeat v{i}={i} thru {}.\n", i + 5))
+ .collect();
+ let end_repeat: Vec<String> = (0..N)
+ .rev()
+ .map(|i| format!("end repeat. /* {i}\n"))
+ .collect();
+
+ let s: String = do_repeat
+ .iter()
+ .chain(end_repeat.iter())
+ .map(|s| s.as_str())
+ .collect();
+ let mut expect_output = vec![
+ (Segment::Identifier, "do"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "v0"),
+ (Segment::Punct, "="),
+ (Segment::Number, "0"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "thru"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "5"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ];
+ for (i, line) in do_repeat.iter().enumerate().take(N).skip(1) {
+ expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
+ if i >= 255 {
+ expect_output.push((Segment::DoRepeatOverflow, ""));
+ }
+ expect_output.push((Segment::Newline, "\n"));
+ }
+ for line in &end_repeat[..254] {
+ expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
+ expect_output.push((Segment::Newline, "\n"));
+ }
+ let comments: Vec<String> = (0..(N - 254)).rev().map(|i| format!("/* {i}")).collect();
+ for comment in &comments {
+ expect_output.extend([
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::EndCommand, "."),
+ (Segment::Spaces, " "),
+ (Segment::Comment, comment),
+ (Segment::Newline, "\n"),
+ ]);
+ }
+
+ let expect_prompts: Vec<_> = (0..N * 2 - 3)
+ .map(|_| PromptStyle::DoRepeat)
+ .chain([PromptStyle::First, PromptStyle::First, PromptStyle::First])
+ .collect();
+ check_segmentation(&s, Syntax::Interactive, &expect_output, &expect_prompts);
+}
+
+#[test]
+fn test_do_repeat_batch() {
+ check_segmentation(
+ r#"do repeat x=a b c
+ y=d e f
+do repeat a=1 thru 5
+another command
+second command
++ third command
+end /* x */ /* y */ repeat print
+end
+ repeat
+do
+ repeat #a=1
+
+ inner command
+end repeat
+"#,
+ Syntax::Batch,
+ &[
+ (Segment::Identifier, "do"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "x"),
+ (Segment::Punct, "="),
+ (Segment::Identifier, "a"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "b"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "c"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "y"),
+ (Segment::Punct, "="),
+ (Segment::Identifier, "d"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "e"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "f"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, ""),
+ (Segment::DoRepeatCommand, "do repeat a=1 thru 5"),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "another command"),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "second command"),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "+ third command"),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, "end /* x */ /* y */ repeat print"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, ""),
+ (Segment::Identifier, "do"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "#a"),
+ (Segment::Punct, "="),
+ (Segment::Number, "1"),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::DoRepeatCommand, " inner command"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "end"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "repeat"),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::DoRepeat,
+ PromptStyle::DoRepeat,
+ PromptStyle::Later,
+ ],
+ );
+}
+
+mod define {
+ use crate::{
+ lex::segment::{Segment, Syntax},
+ prompt::PromptStyle,
+ };
+
+ use super::check_segmentation;
+
+ #[test]
+ fn test_simple() {
+ check_segmentation(
+ r#"define !macro1()
+var1 var2 var3 "!enddefine"
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, "var1 var2 var3 \"!enddefine\""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Define, PromptStyle::Define, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_no_newline_after_parentheses() {
+ check_segmentation(
+ r#"define !macro1() var1 var2 var3 /* !enddefine
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::MacroBody, " var1 var2 var3 /* !enddefine"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Define, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_no_newline_before_enddefine() {
+ check_segmentation(
+ r#"define !macro1()
+var1 var2 var3!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, "var1 var2 var3"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Define, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_all_on_one_line() {
+ check_segmentation(
+ r#"define !macro1()var1 var2 var3!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::MacroBody, "var1 var2 var3"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_empty() {
+ check_segmentation(
+ r#"define !macro1()
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Define, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_blank_lines() {
+ check_segmentation(
+ r#"define !macro1()
+
+
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, ""),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Define,
+ PromptStyle::Define,
+ PromptStyle::Define,
+ PromptStyle::First,
+ ],
+ );
+ }
+
+ #[test]
+ fn test_arguments() {
+ check_segmentation(
+ r#"define !macro1(a(), b(), c())
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Identifier, "a"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Punct, ","),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "b"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Punct, ","),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "c"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Define, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_multiline_arguments() {
+ check_segmentation(
+ r#"define !macro1(
+ a(), b(
+ ),
+ c()
+)
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "a"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Punct, ","),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "b"),
+ (Segment::Punct, "("),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, ")"),
+ (Segment::Punct, ","),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "c"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Define,
+ PromptStyle::First,
+ ],
+ );
+ }
+
+ #[test]
+ fn test_arguments_start_on_second_line() {
+ check_segmentation(
+ r#"define !macro1
+(x,y,z
+)
+content 1
+content 2
+!enddefine.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, "("),
+ (Segment::Identifier, "x"),
+ (Segment::Punct, ","),
+ (Segment::Identifier, "y"),
+ (Segment::Punct, ","),
+ (Segment::Identifier, "z"),
+ (Segment::Newline, "\n"),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, "content 1"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, "content 2"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "!enddefine"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Define,
+ PromptStyle::Define,
+ PromptStyle::Define,
+ PromptStyle::First,
+ ],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_1() {
+ check_segmentation(
+ r#"define !macro1.
+data list /x 1.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "list"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "x"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::First, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_2() {
+ check_segmentation(
+ r#"define !macro1
+x.
+data list /x 1.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "x"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "list"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "x"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Later, PromptStyle::First, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_3() {
+ check_segmentation(
+ r#"define !macro1(.
+x.
+data list /x 1.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "x"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "list"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "x"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::First, PromptStyle::First, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_early_end_of_command_4() {
+ // Notice the command terminator at the end of the `DEFINE` command,
+ // which should not be there and ends it early.
+ check_segmentation(
+ r#"define !macro1.
+data list /x 1.
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "list"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "x"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::First, PromptStyle::First],
+ );
+ }
+
+ #[test]
+ fn test_missing_enddefine() {
+ check_segmentation(
+ r#"define !macro1()
+content line 1
+content line 2
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, "content line 1"),
+ (Segment::Newline, "\n"),
+ (Segment::MacroBody, "content line 2"),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Define,
+ PromptStyle::Define,
+ PromptStyle::Define,
+ ],
+ );
+ }
+
+ #[test]
+ fn test_missing_enddefine_2() {
+ check_segmentation(
+ r#"define !macro1()
+"#,
+ Syntax::Interactive,
+ &[
+ (Segment::Identifier, "define"),
+ (Segment::Spaces, " "),
+ (Segment::MacroName, "!macro1"),
+ (Segment::Punct, "("),
+ (Segment::Punct, ")"),
+ (Segment::Newline, "\n"),
+ ],
+ &[PromptStyle::Define],
+ );
+ }
+}
+
+#[test]
+fn test_batch_mode() {
+ check_segmentation(
+ r#"first command
+ another line of first command
++ second command
+third command
+
+fourth command.
+ fifth command.
+"#,
+ Syntax::Batch,
+ &[
+ (Segment::Identifier, "first"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "another"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "line"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "of"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "first"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "+"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "second"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, ""),
+ (Segment::Identifier, "third"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "fourth"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "fifth"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ ],
+ );
+}
+
+#[test]
+fn test_auto_mode() {
+ check_segmentation(
+ r#"command
+ another line of command
+2sls
++ another command
+another line of second command
+data list /x 1
+aggregate.
+print eject.
+twostep cluster
+
+
+fourth command.
+ fifth command.
+"#,
+ Syntax::Auto,
+ &[
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "another"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "line"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "of"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, ""),
+ (Segment::Number, "2"),
+ (Segment::Identifier, "sls"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, "+"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "another"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "another"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "line"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "of"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "second"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, ""),
+ (Segment::Identifier, "data"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "list"),
+ (Segment::Spaces, " "),
+ (Segment::Punct, "/"),
+ (Segment::Identifier, "x"),
+ (Segment::Spaces, " "),
+ (Segment::Number, "1"),
+ (Segment::Newline, "\n"),
+ (Segment::StartCommand, ""),
+ (Segment::Identifier, "aggregate"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "print"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "eject"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "twostep"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "cluster"),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::SeparateCommands, ""),
+ (Segment::Newline, "\n"),
+ (Segment::Identifier, "fourth"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "fifth"),
+ (Segment::Spaces, " "),
+ (Segment::Identifier, "command"),
+ (Segment::EndCommand, "."),
+ (Segment::Newline, "\n"),
+ ],
+ &[
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::Later,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::Later,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ PromptStyle::First,
+ ],
+ );
+}
/// Check that all negative numbers, even -0, get formatted with a leading `-`.
#[cfg(test)]
-mod test {
+mod tests {
use crate::lex::token::Token;
#[test]
}
#[cfg(test)]
-mod test {
+mod tests {
use crate::output::cairo::{CairoConfig, CairoDriver};
#[test]
mod look_xml;
#[cfg(test)]
-pub mod test;
+pub mod tests;
mod tlo;
/// Areas of a pivot table for styling purposes.
}
#[cfg(test)]
-mod tests {
+mod test {
use crate::output::pivot::{Display26Adic, MetadataEntry, MetadataValue, Value};
#[test]
}
#[cfg(test)]
-mod test {
+mod tests {
use std::str::FromStr;
use quick_xml::de::from_str;
+++ /dev/null
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program. If not, see <http://www.gnu.org/licenses/>.
-
-use std::{fmt::Display, fs::File, path::Path, sync::Arc};
-
-use enum_map::EnumMap;
-
-use crate::output::{
- cairo::{CairoConfig, CairoDriver},
- driver::Driver,
- html::HtmlDriver,
- pivot::{
- Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote,
- FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, LabelPosition,
- Look, PivotTable, RowColBorder, Stroke,
- },
- spv::SpvDriver,
- Details, Item,
-};
-
-use super::{Axis3, Value};
-
-#[test]
-fn color() {
- assert_eq!("#112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
- assert_eq!("112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
- assert_eq!("rgb(11,22,33)".parse(), Ok(Color::new(11, 22, 33)));
- assert_eq!(
- "rgba(11,22,33, 0.25)".parse(),
- Ok(Color::new(11, 22, 33).with_alpha(64))
- );
- assert_eq!("lavender".parse(), Ok(Color::new(230, 230, 250)));
- assert_eq!("transparent".parse(), Ok(Color::new(0, 0, 0).with_alpha(0)));
-}
-
-fn d1(title: &str, axis: Axis3) -> PivotTable {
- let dimension = Dimension::new(
- Group::new("a")
- .with_label_shown()
- .with("a1")
- .with("a2")
- .with("a3"),
- );
- let mut pt = PivotTable::new([(axis, dimension)])
- .with_title(title)
- .with_look(Arc::new(test_look()));
- for i in 0..3 {
- pt.insert(&[i], Value::new_integer(Some(i as f64)));
- }
- pt
-}
-
-#[test]
-fn d1_c() {
- assert_rendering(
- "d1_c",
- &d1("Columns", Axis3::X),
- "\
-Columns
-╭────────╮
-│ a │
-├──┬──┬──┤
-│a1│a2│a3│
-├──┼──┼──┤
-│ 0│ 1│ 2│
-╰──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d1_r() {
- assert_rendering(
- "d1_r",
- &d1("Rows", Axis3::Y),
- "\
-Rows
-╭──┬─╮
-│a │ │
-├──┼─┤
-│a1│0│
-│a2│1│
-│a3│2│
-╰──┴─╯
-",
- );
-}
-
-fn test_look() -> Look {
- let mut look = Look::default();
- look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left);
- look.areas[Area::Title].font_style.bold = false;
- look
-}
-
-fn d2(title: &str, axes: [Axis3; 2], dimension_labels: Option<LabelPosition>) -> PivotTable {
- let d1 = Dimension::new(
- Group::new("a")
- .with_show_label(dimension_labels.is_some())
- .with("a1")
- .with("a2")
- .with("a3"),
- );
-
- let d2 = Dimension::new(
- Group::new("b")
- .with_show_label(dimension_labels.is_some())
- .with("b1")
- .with("b2")
- .with("b3"),
- );
-
- let mut pt = PivotTable::new([(axes[0], d1), (axes[1], d2)]).with_title(title);
- let mut i = 0;
- for b in 0..3 {
- for a in 0..3 {
- pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
- i += 1;
- }
- }
- let look = match dimension_labels {
- Some(position) => test_look().with_row_label_position(position),
- None => test_look(),
- };
- pt.with_look(Arc::new(look))
-}
-
-#[track_caller]
-pub fn assert_lines_eq<E, A>(expected: &str, expected_name: E, actual: &str, actual_name: A)
-where
- E: Display,
- A: Display,
-{
- if expected != actual {
- eprintln!("Unexpected output:\n--- {expected_name}\n+++ {actual_name}");
- for result in diff::lines(expected, &actual) {
- let (prefix, line) = match result {
- diff::Result::Left(line) => ('-', line),
- diff::Result::Both(line, _) => (' ', line),
- diff::Result::Right(line) => ('+', line),
- };
- let suffix = if line.trim_end().len() != line.len() {
- "$"
- } else {
- ""
- };
- eprintln!("{prefix}{line}{suffix}");
- }
- panic!();
- }
-}
-
-#[track_caller]
-pub fn assert_rendering(name: &str, pivot_table: &PivotTable, expected: &str) {
- assert_lines_eq(
- expected,
- format!("{name} expected"),
- &pivot_table.to_string(),
- format!("{name} actual"),
- );
-
- let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
- if let Some(dir) = std::env::var_os("PSPP_TEST_HTML_DIR") {
- let writer = File::create(Path::new(&dir).join(name).with_extension("html")).unwrap();
- HtmlDriver::for_writer(writer).write(&item);
- }
-
- let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
- if let Some(dir) = std::env::var_os("PSPP_TEST_PDF_DIR") {
- let config = CairoConfig::new(Path::new(&dir).join(name).with_extension("pdf"));
- CairoDriver::new(&config).unwrap().write(&item);
- }
-
- if let Some(dir) = std::env::var_os("PSPP_TEST_SPV_DIR") {
- let writer = File::create(Path::new(&dir).join(name).with_extension("spv")).unwrap();
- SpvDriver::for_writer(writer).write(&item);
- }
-}
-
-#[test]
-fn d2_cc() {
- assert_rendering(
- "d2_cc",
- &d2("Columns", [Axis3::X, Axis3::X], None),
- "\
-Columns
-╭────────┬────────┬────────╮
-│ b1 │ b2 │ b3 │
-├──┬──┬──┼──┬──┬──┼──┬──┬──┤
-│a1│a2│a3│a1│a2│a3│a1│a2│a3│
-├──┼──┼──┼──┼──┼──┼──┼──┼──┤
-│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
-╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_cc_with_dim_labels() {
- assert_rendering(
- "d2_cc_with_dim_labels",
- &d2("Columns", [Axis3::X, Axis3::X], Some(LabelPosition::Corner)),
- "\
-Columns
-╭──────────────────────────╮
-│ b │
-├────────┬────────┬────────┤
-│ b1 │ b2 │ b3 │
-├────────┼────────┼────────┤
-│ a │ a │ a │
-├──┬──┬──┼──┬──┬──┼──┬──┬──┤
-│a1│a2│a3│a1│a2│a3│a1│a2│a3│
-├──┼──┼──┼──┼──┼──┼──┼──┼──┤
-│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
-╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_rr() {
- assert_rendering(
- "d2_rr",
- &d2("Rows", [Axis3::Y, Axis3::Y], None),
- "\
-Rows
-╭─────┬─╮
-│b1 a1│0│
-│ a2│1│
-│ a3│2│
-├─────┼─┤
-│b2 a1│3│
-│ a2│4│
-│ a3│5│
-├─────┼─┤
-│b3 a1│6│
-│ a2│7│
-│ a3│8│
-╰─────┴─╯
-",
- );
-}
-
-#[test]
-fn d2_rr_with_corner_dim_labels() {
- assert_rendering(
- "d2_rr_with_corner_dim_labels",
- &d2(
- "Rows - Corner",
- [Axis3::Y, Axis3::Y],
- Some(LabelPosition::Corner),
- ),
- "\
-Rows - Corner
-╭─────┬─╮
-│b a │ │
-├─────┼─┤
-│b1 a1│0│
-│ a2│1│
-│ a3│2│
-├─────┼─┤
-│b2 a1│3│
-│ a2│4│
-│ a3│5│
-├─────┼─┤
-│b3 a1│6│
-│ a2│7│
-│ a3│8│
-╰─────┴─╯
-",
- );
-}
-
-#[test]
-fn d2_rr_with_nested_dim_labels() {
- assert_rendering(
- "d2_rr_with_nested_dim_labels",
- &d2(
- "Rows - Nested",
- [Axis3::Y, Axis3::Y],
- Some(LabelPosition::Nested),
- ),
- "\
-Rows - Nested
-╭─────────┬─╮
-│b b1 a a1│0│
-│ a2│1│
-│ a3│2│
-│ ╶───────┼─┤
-│ b2 a a1│3│
-│ a2│4│
-│ a3│5│
-│ ╶───────┼─┤
-│ b3 a a1│6│
-│ a2│7│
-│ a3│8│
-╰─────────┴─╯
-",
- );
-}
-
-#[test]
-fn d2_cr() {
- assert_rendering(
- "d2_cr",
- &d2("Column x Row", [Axis3::X, Axis3::Y], None),
- "\
-Column x Row
-╭──┬──┬──┬──╮
-│ │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_cr_with_corner_dim_labels() {
- assert_rendering(
- "d2_cr_with_corner_dim_labels",
- &d2(
- "Column x Row - Corner",
- [Axis3::X, Axis3::Y],
- Some(LabelPosition::Corner),
- ),
- "\
-Column x Row - Corner
-╭──┬────────╮
-│ │ a │
-│ ├──┬──┬──┤
-│b │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_cr_with_nested_dim_labels() {
- assert_rendering(
- "d2_cr_with_nested_dim_labels",
- &d2(
- "Column x Row - Nested",
- [Axis3::X, Axis3::Y],
- Some(LabelPosition::Nested),
- ),
- "\
-Column x Row - Nested
-╭────┬────────╮
-│ │ a │
-│ ├──┬──┬──┤
-│ │a1│a2│a3│
-├────┼──┼──┼──┤
-│b b1│ 0│ 1│ 2│
-│ b2│ 3│ 4│ 5│
-│ b3│ 6│ 7│ 8│
-╰────┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_rc() {
- assert_rendering(
- "d2_rc",
- &d2("Row x Column", [Axis3::Y, Axis3::X], None),
- "\
-Row x Column
-╭──┬──┬──┬──╮
-│ │b1│b2│b3│
-├──┼──┼──┼──┤
-│a1│ 0│ 3│ 6│
-│a2│ 1│ 4│ 7│
-│a3│ 2│ 5│ 8│
-╰──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_rc_with_corner_dim_labels() {
- assert_rendering(
- "d2_rc_with_corner_dim_labels",
- &d2(
- "Row x Column - Corner",
- [Axis3::Y, Axis3::X],
- Some(LabelPosition::Corner),
- ),
- "\
-Row x Column - Corner
-╭──┬────────╮
-│ │ b │
-│ ├──┬──┬──┤
-│a │b1│b2│b3│
-├──┼──┼──┼──┤
-│a1│ 0│ 3│ 6│
-│a2│ 1│ 4│ 7│
-│a3│ 2│ 5│ 8│
-╰──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_rc_with_nested_dim_labels() {
- assert_rendering(
- "d2_rc_with_nested_dim_labels",
- &d2(
- "Row x Column - Nested",
- [Axis3::Y, Axis3::X],
- Some(LabelPosition::Nested),
- ),
- "\
-Row x Column - Nested
-╭────┬────────╮
-│ │ b │
-│ ├──┬──┬──┤
-│ │b1│b2│b3│
-├────┼──┼──┼──┤
-│a a1│ 0│ 3│ 6│
-│ a2│ 1│ 4│ 7│
-│ a3│ 2│ 5│ 8│
-╰────┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_cl() {
- let pivot_table = d2("Column x b1", [Axis3::X, Axis3::Z], None);
- assert_rendering(
- "d2_cl-layer0",
- &pivot_table,
- "\
-Column x b1
-b1
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 0│ 1│ 2│
-╰──┴──┴──╯
-",
- );
-
- let pivot_table = pivot_table
- .with_layer(&[1])
- .with_title(Value::new_text("Column x b2"));
- assert_rendering(
- "d2_cl-layer1",
- &pivot_table,
- "\
-Column x b2
-b2
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 3│ 4│ 5│
-╰──┴──┴──╯
-",
- );
-
- let pivot_table = pivot_table
- .with_all_layers()
- .with_title(Value::new_text("Column (All Layers)"));
- assert_rendering(
- "d2_cl-all_layers",
- &pivot_table,
- "\
-Column (All Layers)
-b1
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 0│ 1│ 2│
-╰──┴──┴──╯
-
-Column (All Layers)
-b2
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 3│ 4│ 5│
-╰──┴──┴──╯
-
-Column (All Layers)
-b3
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 6│ 7│ 8│
-╰──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn d2_rl() {
- let pivot_table = d2("Row x b1", [Axis3::Y, Axis3::Z], None);
- assert_rendering(
- "d2_rl-layer0",
- &pivot_table,
- "\
-Row x b1
-b1
-╭──┬─╮
-│a1│0│
-│a2│1│
-│a3│2│
-╰──┴─╯
-",
- );
-
- let pivot_table = pivot_table
- .with_layer(&[1])
- .with_title(Value::new_text("Row x b2"));
- assert_rendering(
- "d2_rl-layer1",
- &pivot_table,
- "\
-Row x b2
-b2
-╭──┬─╮
-│a1│3│
-│a2│4│
-│a3│5│
-╰──┴─╯
-",
- );
-
- let pivot_table = pivot_table
- .with_all_layers()
- .with_title(Value::new_text("Row (All Layers)"));
- assert_rendering(
- "d2_rl-all_layers",
- &pivot_table,
- "\
-Row (All Layers)
-b1
-╭──┬─╮
-│a1│0│
-│a2│1│
-│a3│2│
-╰──┴─╯
-
-Row (All Layers)
-b2
-╭──┬─╮
-│a1│3│
-│a2│4│
-│a3│5│
-╰──┴─╯
-
-Row (All Layers)
-b3
-╭──┬─╮
-│a1│6│
-│a2│7│
-│a3│8│
-╰──┴─╯
-",
- );
-}
-
-#[test]
-fn d3() {
- let a = (
- Axis3::Z,
- Dimension::new(Group::new("a").with("a1").with("a2").with("a3")),
- );
- let b = (
- Axis3::Z,
- Dimension::new(Group::new("b").with("b1").with("b2").with("b3").with("b4")),
- );
- let c = (
- Axis3::X,
- Dimension::new(
- Group::new("c")
- .with("c1")
- .with("c2")
- .with("c3")
- .with("c4")
- .with("c5"),
- ),
- );
- let mut pt = PivotTable::new([a, b, c])
- .with_title("Column x b1 x a1")
- .with_look(Arc::new(test_look()));
- let mut i = 0;
- for c in 0..5 {
- for b in 0..4 {
- for a in 0..3 {
- pt.insert(&[a, b, c], Value::new_integer(Some(i as f64)));
- i += 1;
- }
- }
- }
- assert_rendering(
- "d3-layer0_0",
- &pt,
- "\
-Column x b1 x a1
-b1
-a1
-╭──┬──┬──┬──┬──╮
-│c1│c2│c3│c4│c5│
-├──┼──┼──┼──┼──┤
-│ 0│12│24│36│48│
-╰──┴──┴──┴──┴──╯
-",
- );
-
- let pt = pt.with_layer(&[0, 1]).with_title("Column x b2 x a1");
- assert_rendering(
- "d3-layer0_1",
- &pt,
- "\
-Column x b2 x a1
-b2
-a1
-╭──┬──┬──┬──┬──╮
-│c1│c2│c3│c4│c5│
-├──┼──┼──┼──┼──┤
-│ 3│15│27│39│51│
-╰──┴──┴──┴──┴──╯
-",
- );
-
- let pt = pt.with_layer(&[1, 2]).with_title("Column x b3 x a2");
- assert_rendering(
- "d3-layer1_2",
- &pt,
- "\
-Column x b3 x a2
-b3
-a2
-╭──┬──┬──┬──┬──╮
-│c1│c2│c3│c4│c5│
-├──┼──┼──┼──┼──┤
-│ 7│19│31│43│55│
-╰──┴──┴──┴──┴──╯
-",
- );
-}
-
-#[test]
-fn title_and_caption() {
- let pivot_table =
- d2("Title", [Axis3::X, Axis3::Y], None).with_caption(Value::new_text("Caption"));
- assert_rendering(
- "title_and_caption",
- &pivot_table,
- "\
-Title
-╭──┬──┬──┬──╮
-│ │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-Caption
-",
- );
-
- let pivot_table = pivot_table.with_show_title(false);
- assert_rendering(
- "caption",
- &pivot_table,
- "\
-╭──┬──┬──┬──╮
-│ │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-Caption
-",
- );
-
- let pivot_table = pivot_table.with_show_caption(false);
- assert_rendering(
- "no_title_or_caption",
- &pivot_table,
- "\
-╭──┬──┬──┬──╮
-│ │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-",
- );
-}
-
-fn footnote_table(show_f0: bool) -> PivotTable {
- let mut footnotes = Footnotes::new();
- let f0 = footnotes.push(
- Footnote::new("First footnote")
- .with_marker("*")
- .with_show(show_f0),
- );
- let f1 = footnotes.push(Footnote::new("Second footnote"));
- let a = (
- Axis3::X,
- Dimension::new(
- Group::new(Value::new_text("A").with_footnote(&f0))
- .with_label_shown()
- .with(Value::new_text("B").with_footnote(&f1))
- .with(Value::new_text("C").with_footnote(&f0).with_footnote(&f1)),
- ),
- );
- let d = (
- Axis3::Y,
- Dimension::new(
- Group::new(Value::new_text("D").with_footnote(&f1))
- .with_label_shown()
- .with(Value::new_text("E").with_footnote(&f0))
- .with(Value::new_text("F").with_footnote(&f1).with_footnote(&f0)),
- ),
- );
- let look = test_look().with_row_label_position(LabelPosition::Nested);
- let mut pt = PivotTable::new([a, d]).with_title(
- Value::new_text("Pivot Table with Alphabetic Subscript Footnotes").with_footnote(&f0),
- );
- pt.insert(&[0, 0], Value::new_number(Some(0.0)));
- pt.insert(&[1, 0], Value::new_number(Some(1.0)).with_footnote(&f0));
- pt.insert(&[0, 1], Value::new_number(Some(2.0)).with_footnote(&f1));
- pt.insert(
- &[1, 1],
- Value::new_number(Some(3.0))
- .with_footnote(&f0)
- .with_footnote(&f1),
- );
- pt.with_look(Arc::new(look))
- .with_footnotes(footnotes)
- .with_caption(Value::new_text("Caption").with_footnote(&f0))
- .with_corner_text(
- Value::new_text("Corner")
- .with_footnote(&f0)
- .with_footnote(&f1),
- )
-}
-
-#[test]
-fn footnote_alphabetic_subscript() {
- assert_rendering(
- "footnote_alphabetic_subscript",
- &footnote_table(true),
- "\
-Pivot Table with Alphabetic Subscript Footnotes[*]
-╭────────────┬──────────────────╮
-│ │ A[*] │
-│ ├───────┬──────────┤
-│Corner[*][b]│ B[b] │ C[*][b] │
-├────────────┼───────┼──────────┤
-│D[b] E[*] │ .00│ 1.00[*]│
-│ F[*][b]│2.00[b]│3.00[*][b]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-b. Second footnote
-",
- );
-}
-
-#[test]
-fn footnote_alphabetic_superscript() {
- let mut pt = footnote_table(true);
- let f0 = pt.footnotes.0[0].clone();
- pt = pt.with_title(
- Value::new_text("Pivot Table with Alphabetic Superscript Footnotes").with_footnote(&f0),
- );
- pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
- assert_rendering(
- "footnote_alphabetic_superscript",
- &pt,
- "\
-Pivot Table with Alphabetic Superscript Footnotes[*]
-╭────────────┬──────────────────╮
-│ │ A[*] │
-│ ├───────┬──────────┤
-│Corner[*][b]│ B[b] │ C[*][b] │
-├────────────┼───────┼──────────┤
-│D[b] E[*] │ .00│ 1.00[*]│
-│ F[*][b]│2.00[b]│3.00[*][b]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-b. Second footnote
-",
- );
-}
-
-#[test]
-fn footnote_numeric_subscript() {
- let mut pt = footnote_table(true);
- let f0 = pt.footnotes.0[0].clone();
- pt = pt.with_title(
- Value::new_text("Pivot Table with Numeric Subscript Footnotes").with_footnote(&f0),
- );
- pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
- assert_rendering(
- "footnote_numeric_subscript",
- &pt,
- "\
-Pivot Table with Numeric Subscript Footnotes[*]
-╭────────────┬──────────────────╮
-│ │ A[*] │
-│ ├───────┬──────────┤
-│Corner[*][2]│ B[2] │ C[*][2] │
-├────────────┼───────┼──────────┤
-│D[2] E[*] │ .00│ 1.00[*]│
-│ F[*][2]│2.00[2]│3.00[*][2]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-2. Second footnote
-",
- );
-}
-
-#[test]
-fn footnote_numeric_superscript() {
- let mut pt = footnote_table(true);
- let f0 = pt.footnotes.0[0].clone();
- pt = pt.with_title(
- Value::new_text("Pivot Table with Numeric Superscript Footnotes").with_footnote(&f0),
- );
- pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
- pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
- assert_rendering(
- "footnote_numeric_superscript",
- &pt,
- "\
-Pivot Table with Numeric Superscript Footnotes[*]
-╭────────────┬──────────────────╮
-│ │ A[*] │
-│ ├───────┬──────────┤
-│Corner[*][2]│ B[2] │ C[*][2] │
-├────────────┼───────┼──────────┤
-│D[2] E[*] │ .00│ 1.00[*]│
-│ F[*][2]│2.00[2]│3.00[*][2]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-2. Second footnote
-",
- );
-}
-
-#[test]
-fn footnote_hidden() {
- assert_rendering(
- "footnote_hidden",
- &footnote_table(false),
- "\
-Pivot Table with Alphabetic Subscript Footnotes[*]
-╭────────────┬──────────────────╮
-│ │ A[*] │
-│ ├───────┬──────────┤
-│Corner[*][b]│ B[b] │ C[*][b] │
-├────────────┼───────┼──────────┤
-│D[b] E[*] │ .00│ 1.00[*]│
-│ F[*][b]│2.00[b]│3.00[*][b]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-b. Second footnote
-",
- );
-}
-
-#[test]
-fn no_dimension() {
- let pivot_table = PivotTable::new([])
- .with_title("No Dimensions")
- .with_look(Arc::new(test_look()));
- assert_rendering(
- "no_dimension",
- &pivot_table,
- "No Dimensions
-╭╮
-╰╯
-",
- );
-}
-
-#[test]
-fn empty_dimensions() {
- let look = Arc::new(test_look().with_omit_empty(false));
-
- let d1 = (Axis3::X, Dimension::new(Group::new("a")));
- let pivot_table = PivotTable::new([d1])
- .with_title("One Empty Dimension")
- .with_look(look.clone());
- assert_rendering("one_empty_dimension", &pivot_table, "One Empty Dimension\n");
-
- let d1 = (Axis3::X, Dimension::new(Group::new("a")));
- let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
- let pivot_table = PivotTable::new([d1, d2])
- .with_title("Two Empty Dimensions")
- .with_look(look.clone());
- assert_rendering(
- "two_empty_dimensions",
- &pivot_table,
- "Two Empty Dimensions\n",
- );
-
- let d1 = (Axis3::X, Dimension::new(Group::new("a")));
- let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
- let d3 = (
- Axis3::X,
- Dimension::new(Group::new("c").with("c1").with("c2")),
- );
- let pivot_table = PivotTable::new([d1, d2, d3])
- .with_title("Three Dimensions, Two Empty")
- .with_look(look.clone());
- assert_rendering(
- "three_dimensions_two_empty",
- &pivot_table,
- "Three Dimensions, Two Empty\n",
- );
-}
-
-#[test]
-fn empty_groups() {
- let d1 = (
- Axis3::X,
- Dimension::new(Group::new("a").with("a1").with(Group::new("a2")).with("a3")),
- );
-
- let d2 = (
- Axis3::Y,
- Dimension::new(Group::new("b").with(Group::new("b1")).with("b2").with("b3")),
- );
-
- let mut pt = PivotTable::new([d1, d2]).with_title("Empty Groups");
- let mut i = 0;
- for b in 0..2 {
- for a in 0..2 {
- pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
- i += 1;
- }
- }
- let pivot_table = pt.with_look(Arc::new(test_look().with_omit_empty(false)));
- assert_rendering(
- "empty_groups",
- &pivot_table,
- "\
-Empty Groups
-╭──┬──┬──╮
-│ │a1│a3│
-├──┼──┼──┤
-│b2│ 0│ 1│
-│b3│ 2│ 3│
-╰──┴──┴──╯
-",
- );
-}
-
-fn d4(
- title: &str,
- borders: EnumMap<Border, BorderStyle>,
- show_dimension_labels: bool,
-) -> PivotTable {
- let a = (
- Axis3::X,
- Dimension::new(
- Group::new("a")
- .with_show_label(show_dimension_labels)
- .with("a1")
- .with(Group::new("ag1").with("a2").with("a3")),
- ),
- );
- let b = (
- Axis3::X,
- Dimension::new(
- Group::new("b")
- .with_show_label(show_dimension_labels)
- .with(Group::new("bg1").with("b1").with("b2"))
- .with("b3"),
- ),
- );
- let c = (
- Axis3::Y,
- Dimension::new(
- Group::new("c")
- .with_show_label(show_dimension_labels)
- .with("c1")
- .with(Group::new("cg1").with("c2").with("c3")),
- ),
- );
- let d = (
- Axis3::Y,
- Dimension::new(
- Group::new("d")
- .with_show_label(show_dimension_labels)
- .with(Group::new("dg1").with("d1").with("d2"))
- .with("d3"),
- ),
- );
- let mut pivot_table = PivotTable::new([a, b, c, d])
- .with_title(title)
- .with_look(Arc::new(test_look().with_borders(borders)));
- let mut i = 0;
- for d in 0..3 {
- for c in 0..3 {
- for b in 0..3 {
- for a in 0..3 {
- pivot_table.insert(&[a, b, c, d], Value::new_integer(Some(i as f64)));
- i += 1;
- }
- }
- }
- }
- pivot_table
-}
-
-#[test]
-fn dimension_borders_1() {
- let pivot_table = d4(
- "Dimension Borders 1",
- EnumMap::from_fn(|border| match border {
- Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
- | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
- _ => BorderStyle::none(),
- }),
- true,
- );
- assert_rendering(
- "dimension_borders_1",
- &pivot_table,
- "\
-Dimension Borders 1
- b
- bg1 │
- b1 │ b2 │ b3
- a │ a │ a
- │ ag1 │ │ ag1 │ │ ag1
-d c a1│a2 a3│a1│a2 a3│a1│a2 a3
-dg1 d1 c1 0│ 1 2│ 3│ 4 5│ 6│ 7 8
- ╶─────────┼─────┼──┼─────┼──┼─────
- cg1 c2 9│10 11│12│13 14│15│16 17
- c3 18│19 20│21│22 23│24│25 26
- ╶────────────┼─────┼──┼─────┼──┼─────
- d2 c1 27│28 29│30│31 32│33│34 35
- ╶─────────┼─────┼──┼─────┼──┼─────
- cg1 c2 36│37 38│39│40 41│42│43 44
- c3 45│46 47│48│49 50│51│52 53
-────────────────┼─────┼──┼─────┼──┼─────
- d3 c1 54│55 56│57│58 59│60│61 62
- ╶─────────┼─────┼──┼─────┼──┼─────
- cg1 c2 63│64 65│66│67 68│69│70 71
- c3 72│73 74│75│76 77│78│79 80
-",
- );
-}
-
-#[test]
-fn dimension_borders_2() {
- let pivot_table = d4(
- "Dimension Borders 2",
- EnumMap::from_fn(|border| match border {
- Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
- | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
- _ => BorderStyle::none(),
- }),
- true,
- );
- assert_rendering(
- "dimension_borders_2",
- &pivot_table,
- "\
-Dimension Borders 2
- b
- bg1
- b1 b2 b3
- ╶──────────────────────────
- a a a
- ag1 ag1 ag1
-d c a1 a2 a3 a1 a2 a3 a1 a2 a3
-dg1 d1│ c1 0 1 2 3 4 5 6 7 8
- │cg1 c2 9 10 11 12 13 14 15 16 17
- │ c3 18 19 20 21 22 23 24 25 26
- d2│ c1 27 28 29 30 31 32 33 34 35
- │cg1 c2 36 37 38 39 40 41 42 43 44
- │ c3 45 46 47 48 49 50 51 52 53
- d3│ c1 54 55 56 57 58 59 60 61 62
- │cg1 c2 63 64 65 66 67 68 69 70 71
- │ c3 72 73 74 75 76 77 78 79 80
-",
- );
-}
-
-#[test]
-fn category_borders_1() {
- let pivot_table = d4(
- "Category Borders 1",
- EnumMap::from_fn(|border| match border {
- Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
- | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
- _ => BorderStyle::none(),
- }),
- true,
- );
- assert_rendering(
- "category_borders_1",
- &pivot_table,
- "\
-Category Borders 1
- b
- bg1 ┊
- b1 ┊ b2 ┊ b3
- a ┊ a ┊ a
- ┊ ag1 ┊ ┊ ag1 ┊ ┊ ag1
-d c a1┊a2┊a3┊a1┊a2┊a3┊a1┊a2┊a3
-dg1 d1 c1 0┊ 1┊ 2┊ 3┊ 4┊ 5┊ 6┊ 7┊ 8
- ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- cg1 c2 9┊10┊11┊12┊13┊14┊15┊16┊17
- ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- c3 18┊19┊20┊21┊22┊23┊24┊25┊26
- ╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- d2 c1 27┊28┊29┊30┊31┊32┊33┊34┊35
- ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- cg1 c2 36┊37┊38┊39┊40┊41┊42┊43┊44
- ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- c3 45┊46┊47┊48┊49┊50┊51┊52┊53
-╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- d3 c1 54┊55┊56┊57┊58┊59┊60┊61┊62
- ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- cg1 c2 63┊64┊65┊66┊67┊68┊69┊70┊71
- ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
- c3 72┊73┊74┊75┊76┊77┊78┊79┊80
-",
- );
-}
-
-#[test]
-fn category_borders_2() {
- let pivot_table = d4(
- "Category Borders 2",
- EnumMap::from_fn(|border| match border {
- Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
- | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
- _ => BorderStyle::none(),
- }),
- true,
- );
- assert_rendering(
- "category_borders_2",
- &pivot_table,
- "\
-Category Borders 2
- b
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- bg1
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- b1 b2 b3
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- a a a
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- ag1 ag1 ag1
- ╌╌╌╌╌╌╌ ╌╌╌╌╌╌╌ ╌╌╌╌╌╌
-d c a1 a2 a3 a1 a2 a3 a1 a2 a3
-dg1┊d1┊ c1 0 1 2 3 4 5 6 7 8
- ┊ ┊cg1┊c2 9 10 11 12 13 14 15 16 17
- ┊ ┊ ┊c3 18 19 20 21 22 23 24 25 26
- ┊d2┊ c1 27 28 29 30 31 32 33 34 35
- ┊ ┊cg1┊c2 36 37 38 39 40 41 42 43 44
- ┊ ┊ ┊c3 45 46 47 48 49 50 51 52 53
- d3┊ c1 54 55 56 57 58 59 60 61 62
- ┊cg1┊c2 63 64 65 66 67 68 69 70 71
- ┊ ┊c3 72 73 74 75 76 77 78 79 80
-",
- );
-}
-
-#[test]
-fn category_and_dimension_borders_1() {
- let pivot_table = d4(
- "Category and Dimension Borders 1",
- EnumMap::from_fn(|border| match border {
- Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
- | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
- Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
- | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
- _ => BorderStyle::none(),
- }),
- true,
- );
- assert_rendering(
- "category_and_dimension_borders_1",
- &pivot_table,
- "\
-Category and Dimension Borders 1
- b
- bg1 │
- b1 │ b2 │ b3
- a │ a │ a
- │ ag1 │ │ ag1 │ │ ag1
-d c a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
-dg1 d1 c1 0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
- ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
- cg1 c2 9│10┊11│12│13┊14│15│16┊17
- ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
- c3 18│19┊20│21│22┊23│24│25┊26
- ╶────────────┼──┼──┼──┼──┼──┼──┼──┼──
- d2 c1 27│28┊29│30│31┊32│33│34┊35
- ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
- cg1 c2 36│37┊38│39│40┊41│42│43┊44
- ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
- c3 45│46┊47│48│49┊50│51│52┊53
-────────────────┼──┼──┼──┼──┼──┼──┼──┼──
- d3 c1 54│55┊56│57│58┊59│60│61┊62
- ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
- cg1 c2 63│64┊65│66│67┊68│69│70┊71
- ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
- c3 72│73┊74│75│76┊77│78│79┊80
-",
- );
-}
-
-#[test]
-fn category_and_dimension_borders_2() {
- let pivot_table = d4(
- "Category and Dimension Borders 2",
- EnumMap::from_fn(|border| match border {
- Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
- | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
- Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
- | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
- _ => BorderStyle::none(),
- }),
- true,
- );
- assert_rendering(
- "category_and_dimension_borders_2",
- &pivot_table,
- "\
-Category and Dimension Borders 2
- b
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- bg1
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- b1 b2 b3
- ╶──────────────────────────
- a a a
- ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
- ag1 ag1 ag1
- ╌╌╌╌╌╌╌ ╌╌╌╌╌╌╌ ╌╌╌╌╌╌
-d c a1 a2 a3 a1 a2 a3 a1 a2 a3
-dg1┊d1│ c1 0 1 2 3 4 5 6 7 8
- ┊ │cg1┊c2 9 10 11 12 13 14 15 16 17
- ┊ │ ┊c3 18 19 20 21 22 23 24 25 26
- ┊d2│ c1 27 28 29 30 31 32 33 34 35
- ┊ │cg1┊c2 36 37 38 39 40 41 42 43 44
- ┊ │ ┊c3 45 46 47 48 49 50 51 52 53
- d3│ c1 54 55 56 57 58 59 60 61 62
- │cg1┊c2 63 64 65 66 67 68 69 70 71
- │ ┊c3 72 73 74 75 76 77 78 79 80
-",
- );
-}
-
-const SOLID_BLUE: BorderStyle = BorderStyle {
- stroke: Stroke::Solid,
- color: Color::BLUE,
-};
-
-const DASHED_RED: BorderStyle = BorderStyle {
- stroke: Stroke::Dashed,
- color: Color::RED,
-};
-
-#[test]
-fn category_and_dimension_borders_3() {
- let pivot_table = d4(
- "Category and Dimension Borders 3",
- EnumMap::from_fn(|border| match border {
- Border::Dimension(_) => SOLID_BLUE,
- Border::Category(_) => DASHED_RED,
- _ => BorderStyle::none(),
- }),
- false,
- );
- assert_rendering(
- "category_and_dimension_borders_3",
- &pivot_table,
- "\
-Category and Dimension Borders 3
- bg1 │
- ╌╌╌╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌┤
- b1 │ b2 │ b3
- ╶──┬─────┼──┬─────┼──┬─────
- │ ag1 │ │ ag1 │ │ ag1
- ├╌╌┬╌╌┤ ├╌╌┬╌╌┤ ├╌╌┬╌╌
- a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
-dg1┊d1│ c1 0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
- ┊ ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
- ┊ │cg1┊c2 9│10┊11│12│13┊14│15│16┊17
- ┊ │ ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
- ┊ │ ┊c3 18│19┊20│21│22┊23│24│25┊26
- ├──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
- ┊d2│ c1 27│28┊29│30│31┊32│33│34┊35
- ┊ ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
- ┊ │cg1┊c2 36│37┊38│39│40┊41│42│43┊44
- ┊ │ ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
- ┊ │ ┊c3 45│46┊47│48│49┊50│51│52┊53
-───┴──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
- d3│ c1 54│55┊56│57│58┊59│60│61┊62
- ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
- │cg1┊c2 63│64┊65│66│67┊68│69│70┊71
- │ ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
- │ ┊c3 72│73┊74│75│76┊77│78│79┊80
-",
- );
-}
-
-#[test]
-fn small_numbers() {
- let exponent = (
- Axis3::Y,
- Dimension::new(
- Group::new("exponent")
- .with("0")
- .with("-1")
- .with("-2")
- .with("-3")
- .with("-4")
- .with("-5")
- .with("-6")
- .with("-7")
- .with("-8")
- .with("-9")
- .with_label_shown(),
- ),
- );
- let sign = (
- Axis3::X,
- Dimension::new(
- Group::new("sign")
- .with("positive")
- .with("negative")
- .with_label_shown(),
- ),
- );
- let rc = (
- Axis3::X,
- Dimension::new(
- Group::new("result class")
- .with("general")
- .with("specific")
- .with_label_shown(),
- ),
- );
- let mut pt = PivotTable::new([exponent, sign, rc]).with_title("small numbers");
- pt.insert_number(&[0, 0, 0], Some(1.0), Class::Other);
- pt.insert_number(&[1, 0, 0], Some(0.1), Class::Other);
- pt.insert_number(&[2, 0, 0], Some(0.01), Class::Other);
- pt.insert_number(&[3, 0, 0], Some(0.001), Class::Other);
- pt.insert_number(&[4, 0, 0], Some(0.0001), Class::Other);
- pt.insert_number(&[5, 0, 0], Some(0.00001), Class::Other);
- pt.insert_number(&[6, 0, 0], Some(0.000001), Class::Other);
- pt.insert_number(&[7, 0, 0], Some(0.0000001), Class::Other);
- pt.insert_number(&[8, 0, 0], Some(0.00000001), Class::Other);
- pt.insert_number(&[9, 0, 0], Some(0.000000001), Class::Other);
- pt.insert_number(&[0, 0, 1], Some(-1.0), Class::Residual);
- pt.insert_number(&[1, 0, 1], Some(-0.1), Class::Residual);
- pt.insert_number(&[2, 0, 1], Some(-0.01), Class::Residual);
- pt.insert_number(&[3, 0, 1], Some(-0.001), Class::Residual);
- pt.insert_number(&[4, 0, 1], Some(-0.0001), Class::Residual);
- pt.insert_number(&[5, 0, 1], Some(-0.00001), Class::Residual);
- pt.insert_number(&[6, 0, 1], Some(-0.000001), Class::Residual);
- pt.insert_number(&[7, 0, 1], Some(-0.0000001), Class::Residual);
- pt.insert_number(&[8, 0, 1], Some(-0.00000001), Class::Residual);
- pt.insert_number(&[9, 0, 1], Some(-0.000000001), Class::Residual);
- pt.insert_number(&[0, 1, 0], Some(1.0), Class::Other);
- pt.insert_number(&[1, 1, 0], Some(0.1), Class::Other);
- pt.insert_number(&[2, 1, 0], Some(0.01), Class::Other);
- pt.insert_number(&[3, 1, 0], Some(0.001), Class::Other);
- pt.insert_number(&[4, 1, 0], Some(0.0001), Class::Other);
- pt.insert_number(&[5, 1, 0], Some(0.00001), Class::Other);
- pt.insert_number(&[6, 1, 0], Some(0.000001), Class::Other);
- pt.insert_number(&[7, 1, 0], Some(0.0000001), Class::Other);
- pt.insert_number(&[8, 1, 0], Some(0.00000001), Class::Other);
- pt.insert_number(&[9, 1, 0], Some(0.000000001), Class::Other);
- pt.insert_number(&[0, 1, 1], Some(-1.0), Class::Residual);
- pt.insert_number(&[1, 1, 1], Some(-0.1), Class::Residual);
- pt.insert_number(&[2, 1, 1], Some(-0.01), Class::Residual);
- pt.insert_number(&[3, 1, 1], Some(-0.001), Class::Residual);
- pt.insert_number(&[4, 1, 1], Some(-0.0001), Class::Residual);
- pt.insert_number(&[5, 1, 1], Some(-0.00001), Class::Residual);
- pt.insert_number(&[6, 1, 1], Some(-0.000001), Class::Residual);
- pt.insert_number(&[7, 1, 1], Some(-0.0000001), Class::Residual);
- pt.insert_number(&[8, 1, 1], Some(-0.00000001), Class::Residual);
- pt.insert_number(&[9, 1, 1], Some(-0.000000001), Class::Residual);
- let pivot_table = pt.with_look(Arc::new(test_look()));
- assert_rendering(
- "small_numbers",
- &pivot_table,
- "\
-small numbers
-╭────────┬─────────────────────────────────────╮
-│ │ result class │
-│ ├───────────────────┬─────────────────┤
-│ │ general │ specific │
-│ ├───────────────────┼─────────────────┤
-│ │ sign │ sign │
-│ ├─────────┬─────────┼────────┬────────┤
-│exponent│ positive│ negative│positive│negative│
-├────────┼─────────┼─────────┼────────┼────────┤
-│0 │ 1.00│ 1.00│ -1.00│ -1.00│
-│-1 │ .10│ .10│ -.10│ -.10│
-│-2 │ .01│ .01│ -.01│ -.01│
-│-3 │ .00│ .00│ .00│ .00│
-│-4 │ .00│ .00│ .00│ .00│
-│-5 │1.00E-005│1.00E-005│ .00│ .00│
-│-6 │1.00E-006│1.00E-006│ .00│ .00│
-│-7 │1.00E-007│1.00E-007│ .00│ .00│
-│-8 │1.00E-008│1.00E-008│ .00│ .00│
-│-9 │1.00E-009│1.00E-009│ .00│ .00│
-╰────────┴─────────┴─────────┴────────┴────────╯
-",
- );
-}
--- /dev/null
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see <http://www.gnu.org/licenses/>.
+
+use std::{fmt::Display, fs::File, path::Path, sync::Arc};
+
+use enum_map::EnumMap;
+
+use crate::output::{
+ cairo::{CairoConfig, CairoDriver},
+ driver::Driver,
+ html::HtmlDriver,
+ pivot::{
+ Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote,
+ FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, LabelPosition,
+ Look, PivotTable, RowColBorder, Stroke,
+ },
+ spv::SpvDriver,
+ Details, Item,
+};
+
+use super::{Axis3, Value};
+
+#[test]
+fn color() {
+ assert_eq!("#112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
+ assert_eq!("112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
+ assert_eq!("rgb(11,22,33)".parse(), Ok(Color::new(11, 22, 33)));
+ assert_eq!(
+ "rgba(11,22,33, 0.25)".parse(),
+ Ok(Color::new(11, 22, 33).with_alpha(64))
+ );
+ assert_eq!("lavender".parse(), Ok(Color::new(230, 230, 250)));
+ assert_eq!("transparent".parse(), Ok(Color::new(0, 0, 0).with_alpha(0)));
+}
+
+fn d1(title: &str, axis: Axis3) -> PivotTable {
+ let dimension = Dimension::new(
+ Group::new("a")
+ .with_label_shown()
+ .with("a1")
+ .with("a2")
+ .with("a3"),
+ );
+ let mut pt = PivotTable::new([(axis, dimension)])
+ .with_title(title)
+ .with_look(Arc::new(test_look()));
+ for i in 0..3 {
+ pt.insert(&[i], Value::new_integer(Some(i as f64)));
+ }
+ pt
+}
+
+#[test]
+fn d1_c() {
+ assert_rendering(
+ "d1_c",
+ &d1("Columns", Axis3::X),
+ "\
+Columns
+╭────────╮
+│ a │
+├──┬──┬──┤
+│a1│a2│a3│
+├──┼──┼──┤
+│ 0│ 1│ 2│
+╰──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d1_r() {
+ assert_rendering(
+ "d1_r",
+ &d1("Rows", Axis3::Y),
+ "\
+Rows
+╭──┬─╮
+│a │ │
+├──┼─┤
+│a1│0│
+│a2│1│
+│a3│2│
+╰──┴─╯
+",
+ );
+}
+
+fn test_look() -> Look {
+ let mut look = Look::default();
+ look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left);
+ look.areas[Area::Title].font_style.bold = false;
+ look
+}
+
+fn d2(title: &str, axes: [Axis3; 2], dimension_labels: Option<LabelPosition>) -> PivotTable {
+ let d1 = Dimension::new(
+ Group::new("a")
+ .with_show_label(dimension_labels.is_some())
+ .with("a1")
+ .with("a2")
+ .with("a3"),
+ );
+
+ let d2 = Dimension::new(
+ Group::new("b")
+ .with_show_label(dimension_labels.is_some())
+ .with("b1")
+ .with("b2")
+ .with("b3"),
+ );
+
+ let mut pt = PivotTable::new([(axes[0], d1), (axes[1], d2)]).with_title(title);
+ let mut i = 0;
+ for b in 0..3 {
+ for a in 0..3 {
+ pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
+ i += 1;
+ }
+ }
+ let look = match dimension_labels {
+ Some(position) => test_look().with_row_label_position(position),
+ None => test_look(),
+ };
+ pt.with_look(Arc::new(look))
+}
+
+#[track_caller]
+pub fn assert_lines_eq<E, A>(expected: &str, expected_name: E, actual: &str, actual_name: A)
+where
+ E: Display,
+ A: Display,
+{
+ if expected != actual {
+ eprintln!("Unexpected output:\n--- {expected_name}\n+++ {actual_name}");
+ for result in diff::lines(expected, &actual) {
+ let (prefix, line) = match result {
+ diff::Result::Left(line) => ('-', line),
+ diff::Result::Both(line, _) => (' ', line),
+ diff::Result::Right(line) => ('+', line),
+ };
+ let suffix = if line.trim_end().len() != line.len() {
+ "$"
+ } else {
+ ""
+ };
+ eprintln!("{prefix}{line}{suffix}");
+ }
+ panic!();
+ }
+}
+
+#[track_caller]
+pub fn assert_rendering(name: &str, pivot_table: &PivotTable, expected: &str) {
+ assert_lines_eq(
+ expected,
+ format!("{name} expected"),
+ &pivot_table.to_string(),
+ format!("{name} actual"),
+ );
+
+ let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
+ if let Some(dir) = std::env::var_os("PSPP_TEST_HTML_DIR") {
+ let writer = File::create(Path::new(&dir).join(name).with_extension("html")).unwrap();
+ HtmlDriver::for_writer(writer).write(&item);
+ }
+
+ let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
+ if let Some(dir) = std::env::var_os("PSPP_TEST_PDF_DIR") {
+ let config = CairoConfig::new(Path::new(&dir).join(name).with_extension("pdf"));
+ CairoDriver::new(&config).unwrap().write(&item);
+ }
+
+ if let Some(dir) = std::env::var_os("PSPP_TEST_SPV_DIR") {
+ let writer = File::create(Path::new(&dir).join(name).with_extension("spv")).unwrap();
+ SpvDriver::for_writer(writer).write(&item);
+ }
+}
+
+#[test]
+fn d2_cc() {
+ assert_rendering(
+ "d2_cc",
+ &d2("Columns", [Axis3::X, Axis3::X], None),
+ "\
+Columns
+╭────────┬────────┬────────╮
+│ b1 │ b2 │ b3 │
+├──┬──┬──┼──┬──┬──┼──┬──┬──┤
+│a1│a2│a3│a1│a2│a3│a1│a2│a3│
+├──┼──┼──┼──┼──┼──┼──┼──┼──┤
+│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
+╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_cc_with_dim_labels() {
+ assert_rendering(
+ "d2_cc_with_dim_labels",
+ &d2("Columns", [Axis3::X, Axis3::X], Some(LabelPosition::Corner)),
+ "\
+Columns
+╭──────────────────────────╮
+│ b │
+├────────┬────────┬────────┤
+│ b1 │ b2 │ b3 │
+├────────┼────────┼────────┤
+│ a │ a │ a │
+├──┬──┬──┼──┬──┬──┼──┬──┬──┤
+│a1│a2│a3│a1│a2│a3│a1│a2│a3│
+├──┼──┼──┼──┼──┼──┼──┼──┼──┤
+│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
+╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_rr() {
+ assert_rendering(
+ "d2_rr",
+ &d2("Rows", [Axis3::Y, Axis3::Y], None),
+ "\
+Rows
+╭─────┬─╮
+│b1 a1│0│
+│ a2│1│
+│ a3│2│
+├─────┼─┤
+│b2 a1│3│
+│ a2│4│
+│ a3│5│
+├─────┼─┤
+│b3 a1│6│
+│ a2│7│
+│ a3│8│
+╰─────┴─╯
+",
+ );
+}
+
+#[test]
+fn d2_rr_with_corner_dim_labels() {
+ assert_rendering(
+ "d2_rr_with_corner_dim_labels",
+ &d2(
+ "Rows - Corner",
+ [Axis3::Y, Axis3::Y],
+ Some(LabelPosition::Corner),
+ ),
+ "\
+Rows - Corner
+╭─────┬─╮
+│b a │ │
+├─────┼─┤
+│b1 a1│0│
+│ a2│1│
+│ a3│2│
+├─────┼─┤
+│b2 a1│3│
+│ a2│4│
+│ a3│5│
+├─────┼─┤
+│b3 a1│6│
+│ a2│7│
+│ a3│8│
+╰─────┴─╯
+",
+ );
+}
+
+#[test]
+fn d2_rr_with_nested_dim_labels() {
+ assert_rendering(
+ "d2_rr_with_nested_dim_labels",
+ &d2(
+ "Rows - Nested",
+ [Axis3::Y, Axis3::Y],
+ Some(LabelPosition::Nested),
+ ),
+ "\
+Rows - Nested
+╭─────────┬─╮
+│b b1 a a1│0│
+│ a2│1│
+│ a3│2│
+│ ╶───────┼─┤
+│ b2 a a1│3│
+│ a2│4│
+│ a3│5│
+│ ╶───────┼─┤
+│ b3 a a1│6│
+│ a2│7│
+│ a3│8│
+╰─────────┴─╯
+",
+ );
+}
+
+#[test]
+fn d2_cr() {
+ assert_rendering(
+ "d2_cr",
+ &d2("Column x Row", [Axis3::X, Axis3::Y], None),
+ "\
+Column x Row
+╭──┬──┬──┬──╮
+│ │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_cr_with_corner_dim_labels() {
+ assert_rendering(
+ "d2_cr_with_corner_dim_labels",
+ &d2(
+ "Column x Row - Corner",
+ [Axis3::X, Axis3::Y],
+ Some(LabelPosition::Corner),
+ ),
+ "\
+Column x Row - Corner
+╭──┬────────╮
+│ │ a │
+│ ├──┬──┬──┤
+│b │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_cr_with_nested_dim_labels() {
+ assert_rendering(
+ "d2_cr_with_nested_dim_labels",
+ &d2(
+ "Column x Row - Nested",
+ [Axis3::X, Axis3::Y],
+ Some(LabelPosition::Nested),
+ ),
+ "\
+Column x Row - Nested
+╭────┬────────╮
+│ │ a │
+│ ├──┬──┬──┤
+│ │a1│a2│a3│
+├────┼──┼──┼──┤
+│b b1│ 0│ 1│ 2│
+│ b2│ 3│ 4│ 5│
+│ b3│ 6│ 7│ 8│
+╰────┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_rc() {
+ assert_rendering(
+ "d2_rc",
+ &d2("Row x Column", [Axis3::Y, Axis3::X], None),
+ "\
+Row x Column
+╭──┬──┬──┬──╮
+│ │b1│b2│b3│
+├──┼──┼──┼──┤
+│a1│ 0│ 3│ 6│
+│a2│ 1│ 4│ 7│
+│a3│ 2│ 5│ 8│
+╰──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_rc_with_corner_dim_labels() {
+ assert_rendering(
+ "d2_rc_with_corner_dim_labels",
+ &d2(
+ "Row x Column - Corner",
+ [Axis3::Y, Axis3::X],
+ Some(LabelPosition::Corner),
+ ),
+ "\
+Row x Column - Corner
+╭──┬────────╮
+│ │ b │
+│ ├──┬──┬──┤
+│a │b1│b2│b3│
+├──┼──┼──┼──┤
+│a1│ 0│ 3│ 6│
+│a2│ 1│ 4│ 7│
+│a3│ 2│ 5│ 8│
+╰──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_rc_with_nested_dim_labels() {
+ assert_rendering(
+ "d2_rc_with_nested_dim_labels",
+ &d2(
+ "Row x Column - Nested",
+ [Axis3::Y, Axis3::X],
+ Some(LabelPosition::Nested),
+ ),
+ "\
+Row x Column - Nested
+╭────┬────────╮
+│ │ b │
+│ ├──┬──┬──┤
+│ │b1│b2│b3│
+├────┼──┼──┼──┤
+│a a1│ 0│ 3│ 6│
+│ a2│ 1│ 4│ 7│
+│ a3│ 2│ 5│ 8│
+╰────┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_cl() {
+ let pivot_table = d2("Column x b1", [Axis3::X, Axis3::Z], None);
+ assert_rendering(
+ "d2_cl-layer0",
+ &pivot_table,
+ "\
+Column x b1
+b1
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 0│ 1│ 2│
+╰──┴──┴──╯
+",
+ );
+
+ let pivot_table = pivot_table
+ .with_layer(&[1])
+ .with_title(Value::new_text("Column x b2"));
+ assert_rendering(
+ "d2_cl-layer1",
+ &pivot_table,
+ "\
+Column x b2
+b2
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 3│ 4│ 5│
+╰──┴──┴──╯
+",
+ );
+
+ let pivot_table = pivot_table
+ .with_all_layers()
+ .with_title(Value::new_text("Column (All Layers)"));
+ assert_rendering(
+ "d2_cl-all_layers",
+ &pivot_table,
+ "\
+Column (All Layers)
+b1
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 0│ 1│ 2│
+╰──┴──┴──╯
+
+Column (All Layers)
+b2
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 3│ 4│ 5│
+╰──┴──┴──╯
+
+Column (All Layers)
+b3
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 6│ 7│ 8│
+╰──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn d2_rl() {
+ let pivot_table = d2("Row x b1", [Axis3::Y, Axis3::Z], None);
+ assert_rendering(
+ "d2_rl-layer0",
+ &pivot_table,
+ "\
+Row x b1
+b1
+╭──┬─╮
+│a1│0│
+│a2│1│
+│a3│2│
+╰──┴─╯
+",
+ );
+
+ let pivot_table = pivot_table
+ .with_layer(&[1])
+ .with_title(Value::new_text("Row x b2"));
+ assert_rendering(
+ "d2_rl-layer1",
+ &pivot_table,
+ "\
+Row x b2
+b2
+╭──┬─╮
+│a1│3│
+│a2│4│
+│a3│5│
+╰──┴─╯
+",
+ );
+
+ let pivot_table = pivot_table
+ .with_all_layers()
+ .with_title(Value::new_text("Row (All Layers)"));
+ assert_rendering(
+ "d2_rl-all_layers",
+ &pivot_table,
+ "\
+Row (All Layers)
+b1
+╭──┬─╮
+│a1│0│
+│a2│1│
+│a3│2│
+╰──┴─╯
+
+Row (All Layers)
+b2
+╭──┬─╮
+│a1│3│
+│a2│4│
+│a3│5│
+╰──┴─╯
+
+Row (All Layers)
+b3
+╭──┬─╮
+│a1│6│
+│a2│7│
+│a3│8│
+╰──┴─╯
+",
+ );
+}
+
+#[test]
+fn d3() {
+ let a = (
+ Axis3::Z,
+ Dimension::new(Group::new("a").with("a1").with("a2").with("a3")),
+ );
+ let b = (
+ Axis3::Z,
+ Dimension::new(Group::new("b").with("b1").with("b2").with("b3").with("b4")),
+ );
+ let c = (
+ Axis3::X,
+ Dimension::new(
+ Group::new("c")
+ .with("c1")
+ .with("c2")
+ .with("c3")
+ .with("c4")
+ .with("c5"),
+ ),
+ );
+ let mut pt = PivotTable::new([a, b, c])
+ .with_title("Column x b1 x a1")
+ .with_look(Arc::new(test_look()));
+ let mut i = 0;
+ for c in 0..5 {
+ for b in 0..4 {
+ for a in 0..3 {
+ pt.insert(&[a, b, c], Value::new_integer(Some(i as f64)));
+ i += 1;
+ }
+ }
+ }
+ assert_rendering(
+ "d3-layer0_0",
+ &pt,
+ "\
+Column x b1 x a1
+b1
+a1
+╭──┬──┬──┬──┬──╮
+│c1│c2│c3│c4│c5│
+├──┼──┼──┼──┼──┤
+│ 0│12│24│36│48│
+╰──┴──┴──┴──┴──╯
+",
+ );
+
+ let pt = pt.with_layer(&[0, 1]).with_title("Column x b2 x a1");
+ assert_rendering(
+ "d3-layer0_1",
+ &pt,
+ "\
+Column x b2 x a1
+b2
+a1
+╭──┬──┬──┬──┬──╮
+│c1│c2│c3│c4│c5│
+├──┼──┼──┼──┼──┤
+│ 3│15│27│39│51│
+╰──┴──┴──┴──┴──╯
+",
+ );
+
+ let pt = pt.with_layer(&[1, 2]).with_title("Column x b3 x a2");
+ assert_rendering(
+ "d3-layer1_2",
+ &pt,
+ "\
+Column x b3 x a2
+b3
+a2
+╭──┬──┬──┬──┬──╮
+│c1│c2│c3│c4│c5│
+├──┼──┼──┼──┼──┤
+│ 7│19│31│43│55│
+╰──┴──┴──┴──┴──╯
+",
+ );
+}
+
+#[test]
+fn title_and_caption() {
+ let pivot_table =
+ d2("Title", [Axis3::X, Axis3::Y], None).with_caption(Value::new_text("Caption"));
+ assert_rendering(
+ "title_and_caption",
+ &pivot_table,
+ "\
+Title
+╭──┬──┬──┬──╮
+│ │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+Caption
+",
+ );
+
+ let pivot_table = pivot_table.with_show_title(false);
+ assert_rendering(
+ "caption",
+ &pivot_table,
+ "\
+╭──┬──┬──┬──╮
+│ │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+Caption
+",
+ );
+
+ let pivot_table = pivot_table.with_show_caption(false);
+ assert_rendering(
+ "no_title_or_caption",
+ &pivot_table,
+ "\
+╭──┬──┬──┬──╮
+│ │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+",
+ );
+}
+
+fn footnote_table(show_f0: bool) -> PivotTable {
+ let mut footnotes = Footnotes::new();
+ let f0 = footnotes.push(
+ Footnote::new("First footnote")
+ .with_marker("*")
+ .with_show(show_f0),
+ );
+ let f1 = footnotes.push(Footnote::new("Second footnote"));
+ let a = (
+ Axis3::X,
+ Dimension::new(
+ Group::new(Value::new_text("A").with_footnote(&f0))
+ .with_label_shown()
+ .with(Value::new_text("B").with_footnote(&f1))
+ .with(Value::new_text("C").with_footnote(&f0).with_footnote(&f1)),
+ ),
+ );
+ let d = (
+ Axis3::Y,
+ Dimension::new(
+ Group::new(Value::new_text("D").with_footnote(&f1))
+ .with_label_shown()
+ .with(Value::new_text("E").with_footnote(&f0))
+ .with(Value::new_text("F").with_footnote(&f1).with_footnote(&f0)),
+ ),
+ );
+ let look = test_look().with_row_label_position(LabelPosition::Nested);
+ let mut pt = PivotTable::new([a, d]).with_title(
+ Value::new_text("Pivot Table with Alphabetic Subscript Footnotes").with_footnote(&f0),
+ );
+ pt.insert(&[0, 0], Value::new_number(Some(0.0)));
+ pt.insert(&[1, 0], Value::new_number(Some(1.0)).with_footnote(&f0));
+ pt.insert(&[0, 1], Value::new_number(Some(2.0)).with_footnote(&f1));
+ pt.insert(
+ &[1, 1],
+ Value::new_number(Some(3.0))
+ .with_footnote(&f0)
+ .with_footnote(&f1),
+ );
+ pt.with_look(Arc::new(look))
+ .with_footnotes(footnotes)
+ .with_caption(Value::new_text("Caption").with_footnote(&f0))
+ .with_corner_text(
+ Value::new_text("Corner")
+ .with_footnote(&f0)
+ .with_footnote(&f1),
+ )
+}
+
+#[test]
+fn footnote_alphabetic_subscript() {
+ assert_rendering(
+ "footnote_alphabetic_subscript",
+ &footnote_table(true),
+ "\
+Pivot Table with Alphabetic Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│ │ A[*] │
+│ ├───────┬──────────┤
+│Corner[*][b]│ B[b] │ C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*] │ .00│ 1.00[*]│
+│ F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+b. Second footnote
+",
+ );
+}
+
+#[test]
+fn footnote_alphabetic_superscript() {
+ let mut pt = footnote_table(true);
+ let f0 = pt.footnotes.0[0].clone();
+ pt = pt.with_title(
+ Value::new_text("Pivot Table with Alphabetic Superscript Footnotes").with_footnote(&f0),
+ );
+ pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
+ assert_rendering(
+ "footnote_alphabetic_superscript",
+ &pt,
+ "\
+Pivot Table with Alphabetic Superscript Footnotes[*]
+╭────────────┬──────────────────╮
+│ │ A[*] │
+│ ├───────┬──────────┤
+│Corner[*][b]│ B[b] │ C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*] │ .00│ 1.00[*]│
+│ F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+b. Second footnote
+",
+ );
+}
+
+#[test]
+fn footnote_numeric_subscript() {
+ let mut pt = footnote_table(true);
+ let f0 = pt.footnotes.0[0].clone();
+ pt = pt.with_title(
+ Value::new_text("Pivot Table with Numeric Subscript Footnotes").with_footnote(&f0),
+ );
+ pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
+ assert_rendering(
+ "footnote_numeric_subscript",
+ &pt,
+ "\
+Pivot Table with Numeric Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│ │ A[*] │
+│ ├───────┬──────────┤
+│Corner[*][2]│ B[2] │ C[*][2] │
+├────────────┼───────┼──────────┤
+│D[2] E[*] │ .00│ 1.00[*]│
+│ F[*][2]│2.00[2]│3.00[*][2]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+2. Second footnote
+",
+ );
+}
+
+#[test]
+fn footnote_numeric_superscript() {
+ let mut pt = footnote_table(true);
+ let f0 = pt.footnotes.0[0].clone();
+ pt = pt.with_title(
+ Value::new_text("Pivot Table with Numeric Superscript Footnotes").with_footnote(&f0),
+ );
+ pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
+ pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
+ assert_rendering(
+ "footnote_numeric_superscript",
+ &pt,
+ "\
+Pivot Table with Numeric Superscript Footnotes[*]
+╭────────────┬──────────────────╮
+│ │ A[*] │
+│ ├───────┬──────────┤
+│Corner[*][2]│ B[2] │ C[*][2] │
+├────────────┼───────┼──────────┤
+│D[2] E[*] │ .00│ 1.00[*]│
+│ F[*][2]│2.00[2]│3.00[*][2]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+2. Second footnote
+",
+ );
+}
+
+#[test]
+fn footnote_hidden() {
+ assert_rendering(
+ "footnote_hidden",
+ &footnote_table(false),
+ "\
+Pivot Table with Alphabetic Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│ │ A[*] │
+│ ├───────┬──────────┤
+│Corner[*][b]│ B[b] │ C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*] │ .00│ 1.00[*]│
+│ F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+b. Second footnote
+",
+ );
+}
+
+#[test]
+fn no_dimension() {
+ let pivot_table = PivotTable::new([])
+ .with_title("No Dimensions")
+ .with_look(Arc::new(test_look()));
+ assert_rendering(
+ "no_dimension",
+ &pivot_table,
+ "No Dimensions
+╭╮
+╰╯
+",
+ );
+}
+
+#[test]
+fn empty_dimensions() {
+ let look = Arc::new(test_look().with_omit_empty(false));
+
+ let d1 = (Axis3::X, Dimension::new(Group::new("a")));
+ let pivot_table = PivotTable::new([d1])
+ .with_title("One Empty Dimension")
+ .with_look(look.clone());
+ assert_rendering("one_empty_dimension", &pivot_table, "One Empty Dimension\n");
+
+ let d1 = (Axis3::X, Dimension::new(Group::new("a")));
+ let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
+ let pivot_table = PivotTable::new([d1, d2])
+ .with_title("Two Empty Dimensions")
+ .with_look(look.clone());
+ assert_rendering(
+ "two_empty_dimensions",
+ &pivot_table,
+ "Two Empty Dimensions\n",
+ );
+
+ let d1 = (Axis3::X, Dimension::new(Group::new("a")));
+ let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
+ let d3 = (
+ Axis3::X,
+ Dimension::new(Group::new("c").with("c1").with("c2")),
+ );
+ let pivot_table = PivotTable::new([d1, d2, d3])
+ .with_title("Three Dimensions, Two Empty")
+ .with_look(look.clone());
+ assert_rendering(
+ "three_dimensions_two_empty",
+ &pivot_table,
+ "Three Dimensions, Two Empty\n",
+ );
+}
+
+#[test]
+fn empty_groups() {
+ let d1 = (
+ Axis3::X,
+ Dimension::new(Group::new("a").with("a1").with(Group::new("a2")).with("a3")),
+ );
+
+ let d2 = (
+ Axis3::Y,
+ Dimension::new(Group::new("b").with(Group::new("b1")).with("b2").with("b3")),
+ );
+
+ let mut pt = PivotTable::new([d1, d2]).with_title("Empty Groups");
+ let mut i = 0;
+ for b in 0..2 {
+ for a in 0..2 {
+ pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
+ i += 1;
+ }
+ }
+ let pivot_table = pt.with_look(Arc::new(test_look().with_omit_empty(false)));
+ assert_rendering(
+ "empty_groups",
+ &pivot_table,
+ "\
+Empty Groups
+╭──┬──┬──╮
+│ │a1│a3│
+├──┼──┼──┤
+│b2│ 0│ 1│
+│b3│ 2│ 3│
+╰──┴──┴──╯
+",
+ );
+}
+
+fn d4(
+ title: &str,
+ borders: EnumMap<Border, BorderStyle>,
+ show_dimension_labels: bool,
+) -> PivotTable {
+ let a = (
+ Axis3::X,
+ Dimension::new(
+ Group::new("a")
+ .with_show_label(show_dimension_labels)
+ .with("a1")
+ .with(Group::new("ag1").with("a2").with("a3")),
+ ),
+ );
+ let b = (
+ Axis3::X,
+ Dimension::new(
+ Group::new("b")
+ .with_show_label(show_dimension_labels)
+ .with(Group::new("bg1").with("b1").with("b2"))
+ .with("b3"),
+ ),
+ );
+ let c = (
+ Axis3::Y,
+ Dimension::new(
+ Group::new("c")
+ .with_show_label(show_dimension_labels)
+ .with("c1")
+ .with(Group::new("cg1").with("c2").with("c3")),
+ ),
+ );
+ let d = (
+ Axis3::Y,
+ Dimension::new(
+ Group::new("d")
+ .with_show_label(show_dimension_labels)
+ .with(Group::new("dg1").with("d1").with("d2"))
+ .with("d3"),
+ ),
+ );
+ let mut pivot_table = PivotTable::new([a, b, c, d])
+ .with_title(title)
+ .with_look(Arc::new(test_look().with_borders(borders)));
+ let mut i = 0;
+ for d in 0..3 {
+ for c in 0..3 {
+ for b in 0..3 {
+ for a in 0..3 {
+ pivot_table.insert(&[a, b, c, d], Value::new_integer(Some(i as f64)));
+ i += 1;
+ }
+ }
+ }
+ }
+ pivot_table
+}
+
+#[test]
+fn dimension_borders_1() {
+ let pivot_table = d4(
+ "Dimension Borders 1",
+ EnumMap::from_fn(|border| match border {
+ Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
+ | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
+ _ => BorderStyle::none(),
+ }),
+ true,
+ );
+ assert_rendering(
+ "dimension_borders_1",
+ &pivot_table,
+ "\
+Dimension Borders 1
+ b
+ bg1 │
+ b1 │ b2 │ b3
+ a │ a │ a
+ │ ag1 │ │ ag1 │ │ ag1
+d c a1│a2 a3│a1│a2 a3│a1│a2 a3
+dg1 d1 c1 0│ 1 2│ 3│ 4 5│ 6│ 7 8
+ ╶─────────┼─────┼──┼─────┼──┼─────
+ cg1 c2 9│10 11│12│13 14│15│16 17
+ c3 18│19 20│21│22 23│24│25 26
+ ╶────────────┼─────┼──┼─────┼──┼─────
+ d2 c1 27│28 29│30│31 32│33│34 35
+ ╶─────────┼─────┼──┼─────┼──┼─────
+ cg1 c2 36│37 38│39│40 41│42│43 44
+ c3 45│46 47│48│49 50│51│52 53
+────────────────┼─────┼──┼─────┼──┼─────
+ d3 c1 54│55 56│57│58 59│60│61 62
+ ╶─────────┼─────┼──┼─────┼──┼─────
+ cg1 c2 63│64 65│66│67 68│69│70 71
+ c3 72│73 74│75│76 77│78│79 80
+",
+ );
+}
+
+#[test]
+fn dimension_borders_2() {
+ let pivot_table = d4(
+ "Dimension Borders 2",
+ EnumMap::from_fn(|border| match border {
+ Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+ | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
+ _ => BorderStyle::none(),
+ }),
+ true,
+ );
+ assert_rendering(
+ "dimension_borders_2",
+ &pivot_table,
+ "\
+Dimension Borders 2
+ b
+ bg1
+ b1 b2 b3
+ ╶──────────────────────────
+ a a a
+ ag1 ag1 ag1
+d c a1 a2 a3 a1 a2 a3 a1 a2 a3
+dg1 d1│ c1 0 1 2 3 4 5 6 7 8
+ │cg1 c2 9 10 11 12 13 14 15 16 17
+ │ c3 18 19 20 21 22 23 24 25 26
+ d2│ c1 27 28 29 30 31 32 33 34 35
+ │cg1 c2 36 37 38 39 40 41 42 43 44
+ │ c3 45 46 47 48 49 50 51 52 53
+ d3│ c1 54 55 56 57 58 59 60 61 62
+ │cg1 c2 63 64 65 66 67 68 69 70 71
+ │ c3 72 73 74 75 76 77 78 79 80
+",
+ );
+}
+
+#[test]
+fn category_borders_1() {
+ let pivot_table = d4(
+ "Category Borders 1",
+ EnumMap::from_fn(|border| match border {
+ Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
+ | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
+ _ => BorderStyle::none(),
+ }),
+ true,
+ );
+ assert_rendering(
+ "category_borders_1",
+ &pivot_table,
+ "\
+Category Borders 1
+ b
+ bg1 ┊
+ b1 ┊ b2 ┊ b3
+ a ┊ a ┊ a
+ ┊ ag1 ┊ ┊ ag1 ┊ ┊ ag1
+d c a1┊a2┊a3┊a1┊a2┊a3┊a1┊a2┊a3
+dg1 d1 c1 0┊ 1┊ 2┊ 3┊ 4┊ 5┊ 6┊ 7┊ 8
+ ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ cg1 c2 9┊10┊11┊12┊13┊14┊15┊16┊17
+ ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ c3 18┊19┊20┊21┊22┊23┊24┊25┊26
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ d2 c1 27┊28┊29┊30┊31┊32┊33┊34┊35
+ ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ cg1 c2 36┊37┊38┊39┊40┊41┊42┊43┊44
+ ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ c3 45┊46┊47┊48┊49┊50┊51┊52┊53
+╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ d3 c1 54┊55┊56┊57┊58┊59┊60┊61┊62
+ ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ cg1 c2 63┊64┊65┊66┊67┊68┊69┊70┊71
+ ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+ c3 72┊73┊74┊75┊76┊77┊78┊79┊80
+",
+ );
+}
+
+#[test]
+fn category_borders_2() {
+ let pivot_table = d4(
+ "Category Borders 2",
+ EnumMap::from_fn(|border| match border {
+ Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+ | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
+ _ => BorderStyle::none(),
+ }),
+ true,
+ );
+ assert_rendering(
+ "category_borders_2",
+ &pivot_table,
+ "\
+Category Borders 2
+ b
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ bg1
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ b1 b2 b3
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ a a a
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ ag1 ag1 ag1
+ ╌╌╌╌╌╌╌ ╌╌╌╌╌╌╌ ╌╌╌╌╌╌
+d c a1 a2 a3 a1 a2 a3 a1 a2 a3
+dg1┊d1┊ c1 0 1 2 3 4 5 6 7 8
+ ┊ ┊cg1┊c2 9 10 11 12 13 14 15 16 17
+ ┊ ┊ ┊c3 18 19 20 21 22 23 24 25 26
+ ┊d2┊ c1 27 28 29 30 31 32 33 34 35
+ ┊ ┊cg1┊c2 36 37 38 39 40 41 42 43 44
+ ┊ ┊ ┊c3 45 46 47 48 49 50 51 52 53
+ d3┊ c1 54 55 56 57 58 59 60 61 62
+ ┊cg1┊c2 63 64 65 66 67 68 69 70 71
+ ┊ ┊c3 72 73 74 75 76 77 78 79 80
+",
+ );
+}
+
+#[test]
+fn category_and_dimension_borders_1() {
+ let pivot_table = d4(
+ "Category and Dimension Borders 1",
+ EnumMap::from_fn(|border| match border {
+ Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
+ | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
+ Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
+ | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
+ _ => BorderStyle::none(),
+ }),
+ true,
+ );
+ assert_rendering(
+ "category_and_dimension_borders_1",
+ &pivot_table,
+ "\
+Category and Dimension Borders 1
+ b
+ bg1 │
+ b1 │ b2 │ b3
+ a │ a │ a
+ │ ag1 │ │ ag1 │ │ ag1
+d c a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
+dg1 d1 c1 0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
+ ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
+ cg1 c2 9│10┊11│12│13┊14│15│16┊17
+ ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+ c3 18│19┊20│21│22┊23│24│25┊26
+ ╶────────────┼──┼──┼──┼──┼──┼──┼──┼──
+ d2 c1 27│28┊29│30│31┊32│33│34┊35
+ ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
+ cg1 c2 36│37┊38│39│40┊41│42│43┊44
+ ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+ c3 45│46┊47│48│49┊50│51│52┊53
+────────────────┼──┼──┼──┼──┼──┼──┼──┼──
+ d3 c1 54│55┊56│57│58┊59│60│61┊62
+ ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
+ cg1 c2 63│64┊65│66│67┊68│69│70┊71
+ ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+ c3 72│73┊74│75│76┊77│78│79┊80
+",
+ );
+}
+
+#[test]
+fn category_and_dimension_borders_2() {
+ let pivot_table = d4(
+ "Category and Dimension Borders 2",
+ EnumMap::from_fn(|border| match border {
+ Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+ | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
+ Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+ | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
+ _ => BorderStyle::none(),
+ }),
+ true,
+ );
+ assert_rendering(
+ "category_and_dimension_borders_2",
+ &pivot_table,
+ "\
+Category and Dimension Borders 2
+ b
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ bg1
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ b1 b2 b3
+ ╶──────────────────────────
+ a a a
+ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+ ag1 ag1 ag1
+ ╌╌╌╌╌╌╌ ╌╌╌╌╌╌╌ ╌╌╌╌╌╌
+d c a1 a2 a3 a1 a2 a3 a1 a2 a3
+dg1┊d1│ c1 0 1 2 3 4 5 6 7 8
+ ┊ │cg1┊c2 9 10 11 12 13 14 15 16 17
+ ┊ │ ┊c3 18 19 20 21 22 23 24 25 26
+ ┊d2│ c1 27 28 29 30 31 32 33 34 35
+ ┊ │cg1┊c2 36 37 38 39 40 41 42 43 44
+ ┊ │ ┊c3 45 46 47 48 49 50 51 52 53
+ d3│ c1 54 55 56 57 58 59 60 61 62
+ │cg1┊c2 63 64 65 66 67 68 69 70 71
+ │ ┊c3 72 73 74 75 76 77 78 79 80
+",
+ );
+}
+
+const SOLID_BLUE: BorderStyle = BorderStyle {
+ stroke: Stroke::Solid,
+ color: Color::BLUE,
+};
+
+const DASHED_RED: BorderStyle = BorderStyle {
+ stroke: Stroke::Dashed,
+ color: Color::RED,
+};
+
+#[test]
+fn category_and_dimension_borders_3() {
+ let pivot_table = d4(
+ "Category and Dimension Borders 3",
+ EnumMap::from_fn(|border| match border {
+ Border::Dimension(_) => SOLID_BLUE,
+ Border::Category(_) => DASHED_RED,
+ _ => BorderStyle::none(),
+ }),
+ false,
+ );
+ assert_rendering(
+ "category_and_dimension_borders_3",
+ &pivot_table,
+ "\
+Category and Dimension Borders 3
+ bg1 │
+ ╌╌╌╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌┤
+ b1 │ b2 │ b3
+ ╶──┬─────┼──┬─────┼──┬─────
+ │ ag1 │ │ ag1 │ │ ag1
+ ├╌╌┬╌╌┤ ├╌╌┬╌╌┤ ├╌╌┬╌╌
+ a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
+dg1┊d1│ c1 0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
+ ┊ ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
+ ┊ │cg1┊c2 9│10┊11│12│13┊14│15│16┊17
+ ┊ │ ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+ ┊ │ ┊c3 18│19┊20│21│22┊23│24│25┊26
+ ├──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
+ ┊d2│ c1 27│28┊29│30│31┊32│33│34┊35
+ ┊ ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
+ ┊ │cg1┊c2 36│37┊38│39│40┊41│42│43┊44
+ ┊ │ ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+ ┊ │ ┊c3 45│46┊47│48│49┊50│51│52┊53
+───┴──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
+ d3│ c1 54│55┊56│57│58┊59│60│61┊62
+ ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
+ │cg1┊c2 63│64┊65│66│67┊68│69│70┊71
+ │ ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+ │ ┊c3 72│73┊74│75│76┊77│78│79┊80
+",
+ );
+}
+
+#[test]
+fn small_numbers() {
+ let exponent = (
+ Axis3::Y,
+ Dimension::new(
+ Group::new("exponent")
+ .with("0")
+ .with("-1")
+ .with("-2")
+ .with("-3")
+ .with("-4")
+ .with("-5")
+ .with("-6")
+ .with("-7")
+ .with("-8")
+ .with("-9")
+ .with_label_shown(),
+ ),
+ );
+ let sign = (
+ Axis3::X,
+ Dimension::new(
+ Group::new("sign")
+ .with("positive")
+ .with("negative")
+ .with_label_shown(),
+ ),
+ );
+ let rc = (
+ Axis3::X,
+ Dimension::new(
+ Group::new("result class")
+ .with("general")
+ .with("specific")
+ .with_label_shown(),
+ ),
+ );
+ let mut pt = PivotTable::new([exponent, sign, rc]).with_title("small numbers");
+ pt.insert_number(&[0, 0, 0], Some(1.0), Class::Other);
+ pt.insert_number(&[1, 0, 0], Some(0.1), Class::Other);
+ pt.insert_number(&[2, 0, 0], Some(0.01), Class::Other);
+ pt.insert_number(&[3, 0, 0], Some(0.001), Class::Other);
+ pt.insert_number(&[4, 0, 0], Some(0.0001), Class::Other);
+ pt.insert_number(&[5, 0, 0], Some(0.00001), Class::Other);
+ pt.insert_number(&[6, 0, 0], Some(0.000001), Class::Other);
+ pt.insert_number(&[7, 0, 0], Some(0.0000001), Class::Other);
+ pt.insert_number(&[8, 0, 0], Some(0.00000001), Class::Other);
+ pt.insert_number(&[9, 0, 0], Some(0.000000001), Class::Other);
+ pt.insert_number(&[0, 0, 1], Some(-1.0), Class::Residual);
+ pt.insert_number(&[1, 0, 1], Some(-0.1), Class::Residual);
+ pt.insert_number(&[2, 0, 1], Some(-0.01), Class::Residual);
+ pt.insert_number(&[3, 0, 1], Some(-0.001), Class::Residual);
+ pt.insert_number(&[4, 0, 1], Some(-0.0001), Class::Residual);
+ pt.insert_number(&[5, 0, 1], Some(-0.00001), Class::Residual);
+ pt.insert_number(&[6, 0, 1], Some(-0.000001), Class::Residual);
+ pt.insert_number(&[7, 0, 1], Some(-0.0000001), Class::Residual);
+ pt.insert_number(&[8, 0, 1], Some(-0.00000001), Class::Residual);
+ pt.insert_number(&[9, 0, 1], Some(-0.000000001), Class::Residual);
+ pt.insert_number(&[0, 1, 0], Some(1.0), Class::Other);
+ pt.insert_number(&[1, 1, 0], Some(0.1), Class::Other);
+ pt.insert_number(&[2, 1, 0], Some(0.01), Class::Other);
+ pt.insert_number(&[3, 1, 0], Some(0.001), Class::Other);
+ pt.insert_number(&[4, 1, 0], Some(0.0001), Class::Other);
+ pt.insert_number(&[5, 1, 0], Some(0.00001), Class::Other);
+ pt.insert_number(&[6, 1, 0], Some(0.000001), Class::Other);
+ pt.insert_number(&[7, 1, 0], Some(0.0000001), Class::Other);
+ pt.insert_number(&[8, 1, 0], Some(0.00000001), Class::Other);
+ pt.insert_number(&[9, 1, 0], Some(0.000000001), Class::Other);
+ pt.insert_number(&[0, 1, 1], Some(-1.0), Class::Residual);
+ pt.insert_number(&[1, 1, 1], Some(-0.1), Class::Residual);
+ pt.insert_number(&[2, 1, 1], Some(-0.01), Class::Residual);
+ pt.insert_number(&[3, 1, 1], Some(-0.001), Class::Residual);
+ pt.insert_number(&[4, 1, 1], Some(-0.0001), Class::Residual);
+ pt.insert_number(&[5, 1, 1], Some(-0.00001), Class::Residual);
+ pt.insert_number(&[6, 1, 1], Some(-0.000001), Class::Residual);
+ pt.insert_number(&[7, 1, 1], Some(-0.0000001), Class::Residual);
+ pt.insert_number(&[8, 1, 1], Some(-0.00000001), Class::Residual);
+ pt.insert_number(&[9, 1, 1], Some(-0.000000001), Class::Residual);
+ let pivot_table = pt.with_look(Arc::new(test_look()));
+ assert_rendering(
+ "small_numbers",
+ &pivot_table,
+ "\
+small numbers
+╭────────┬─────────────────────────────────────╮
+│ │ result class │
+│ ├───────────────────┬─────────────────┤
+│ │ general │ specific │
+│ ├───────────────────┼─────────────────┤
+│ │ sign │ sign │
+│ ├─────────┬─────────┼────────┬────────┤
+│exponent│ positive│ negative│positive│negative│
+├────────┼─────────┼─────────┼────────┼────────┤
+│0 │ 1.00│ 1.00│ -1.00│ -1.00│
+│-1 │ .10│ .10│ -.10│ -.10│
+│-2 │ .01│ .01│ -.01│ -.01│
+│-3 │ .00│ .00│ .00│ .00│
+│-4 │ .00│ .00│ .00│ .00│
+│-5 │1.00E-005│1.00E-005│ .00│ .00│
+│-6 │1.00E-006│1.00E-006│ .00│ .00│
+│-7 │1.00E-007│1.00E-007│ .00│ .00│
+│-8 │1.00E-008│1.00E-008│ .00│ .00│
+│-9 │1.00E-009│1.00E-009│ .00│ .00│
+╰────────┴─────────┴─────────┴────────┴────────╯
+",
+ );
+}
}
#[cfg(test)]
-mod test {
+mod tests {
use crate::output::pivot::tlo::parse_tlo;
#[test]
}
#[cfg(test)]
-mod test {
+mod tests {
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::output::text::new_line_breaks;
}
#[cfg(test)]
-mod test {
+mod tests {
use super::{Emphasis, TextLine};
use enum_iterator::all;
pub use write::{SystemFileVersion, WriteOptions, Writer};
#[cfg(test)]
-mod test;
+mod tests;
fn serialize_endian<S>(endian: &Endian, serializer: S) -> Result<S::Ok, S::Error>
where
}
#[cfg(test)]
-mod test {
+mod tests {
use crate::sys::sack::sack;
use anyhow::Result;
use binrw::Endian;
+++ /dev/null
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program. If not, see <http://www.gnu.org/licenses/>.
-
-use std::{
- fs::File,
- io::{BufRead, BufReader, Cursor, Seek},
- path::{Path, PathBuf},
- sync::Arc,
-};
-
-use binrw::Endian;
-use encoding_rs::UTF_8;
-use itertools::Itertools;
-
-use crate::{
- crypto::EncryptedFile,
- data::Datum,
- dictionary::Dictionary,
- identifier::Identifier,
- output::{
- pivot::{test::assert_lines_eq, Axis3, Dimension, Group, PivotTable, Value},
- Details, Item, Text,
- },
- sys::{
- cooked::ReadOptions,
- raw::{self, records::Compression, ErrorDetails},
- sack::sack,
- WriteOptions,
- },
- variable::{VarWidth, Variable},
-};
-
-#[test]
-fn variable_labels_and_missing_values() {
- test_sack_sysfile("variable_labels_and_missing_values");
-}
-
-#[test]
-fn unspecified_number_of_variable_positions() {
- test_sack_sysfile("unspecified_number_of_variable_positions");
-}
-
-#[test]
-fn wrong_variable_positions_but_v13() {
- test_sack_sysfile("wrong_variable_positions_but_v13");
-}
-
-#[test]
-fn value_labels() {
- test_sack_sysfile("value_labels");
-}
-
-#[test]
-fn documents() {
- test_sack_sysfile("documents");
-}
-
-#[test]
-fn empty_document_record() {
- test_sack_sysfile("empty_document_record");
-}
-
-#[test]
-fn variable_sets() {
- test_sack_sysfile("variable_sets");
-}
-
-#[test]
-fn multiple_response_sets() {
- test_sack_sysfile("multiple_response_sets");
-}
-
-#[test]
-fn extra_product_info() {
- // Also checks for handling of CR-only line ends in file label and extra
- // product info.
- test_sack_sysfile("extra_product_info");
-}
-
-#[test]
-fn variable_display_without_width() {
- test_sack_sysfile("variable_display_without_width");
-}
-
-#[test]
-fn variable_display_with_width() {
- test_sack_sysfile("variable_display_with_width");
-}
-
-#[test]
-fn long_variable_names() {
- test_sack_sysfile("long_variable_names");
-}
-
-#[test]
-fn very_long_strings() {
- test_sack_sysfile("very_long_strings");
-}
-
-#[test]
-fn attributes() {
- test_sack_sysfile("attributes");
-}
-
-#[test]
-fn variable_roles() {
- test_sack_sysfile("variable_roles");
-}
-
-#[test]
-fn compressed_data() {
- test_sack_sysfile("compressed_data");
-}
-
-#[test]
-fn compressed_data_zero_bias() {
- test_sack_sysfile("compressed_data_zero_bias");
-}
-
-#[test]
-fn compressed_data_other_bias() {
- test_sack_sysfile("compressed_data_other_bias");
-}
-
-#[test]
-fn zcompressed_data() {
- test_sack_sysfile("zcompressed_data");
-}
-
-#[test]
-fn no_variables() {
- test_sack_sysfile("no_variables");
-}
-
-#[test]
-fn unknown_encoding() {
- test_sack_sysfile("unknown_encoding");
-}
-
-#[test]
-fn misplaced_type_4_record() {
- test_sack_sysfile("misplaced_type_4_record");
-}
-
-#[test]
-fn bad_record_type() {
- test_sack_sysfile("bad_record_type");
-}
-
-#[test]
-fn wrong_variable_positions() {
- test_sack_sysfile("wrong_variable_positions");
-}
-
-#[test]
-fn invalid_variable_name() {
- test_sack_sysfile("invalid_variable_name");
-}
-
-#[test]
-fn invalid_label_indicator() {
- test_sack_sysfile("invalid_label_indicator");
-}
-
-#[test]
-fn invalid_missing_indicator() {
- test_sack_sysfile("invalid_missing_indicator");
-}
-
-#[test]
-fn invalid_missing_indicator2() {
- test_sack_sysfile("invalid_missing_indicator2");
-}
-
-#[test]
-fn missing_string_continuation() {
- test_sack_sysfile("missing_string_continuation");
-}
-
-#[test]
-fn invalid_variable_format() {
- test_sack_sysfile("invalid_variable_format");
-}
-
-#[test]
-fn invalid_long_string_missing_values() {
- test_sack_sysfile("invalid_long_string_missing_values");
-}
-
-#[test]
-fn weight_must_be_numeric() {
- test_sack_sysfile("weight_must_be_numeric");
-}
-
-#[test]
-fn weight_variable_bad_index() {
- test_sack_sysfile("weight_variable_bad_index");
-}
-
-#[test]
-fn weight_variable_continuation() {
- test_sack_sysfile("weight_variable_continuation");
-}
-
-#[test]
-fn multiple_documents_records() {
- test_sack_sysfile("multiple_documents_records");
-}
-
-#[test]
-fn unknown_extension_record() {
- test_sack_sysfile("unknown_extension_record");
-}
-
-#[test]
-fn extension_too_large() {
- test_sack_sysfile("extension_too_large");
-}
-
-#[test]
-fn bad_machine_integer_info_count() {
- test_sack_sysfile("bad_machine_integer_info_count");
-}
-
-#[test]
-fn bad_machine_integer_info_float_format() {
- test_sack_sysfile("bad_machine_integer_info_float_format");
-}
-
-#[test]
-fn bad_machine_integer_info_endianness() {
- test_sack_sysfile("bad_machine_integer_info_endianness");
-}
-
-#[test]
-fn bad_machine_float_info_size() {
- test_sack_sysfile("bad_machine_float_info_size");
-}
-
-#[test]
-fn wrong_special_floats() {
- test_sack_sysfile("wrong_special_floats");
-}
-
-#[test]
-fn variable_sets_unknown_variable() {
- test_sack_sysfile("variable_sets_unknown_variable");
-}
-
-#[test]
-fn multiple_response_sets_bad_name() {
- test_sack_sysfile("multiple_response_sets_bad_name");
-}
-
-#[test]
-fn multiple_response_sets_missing_space_after_c() {
- test_sack_sysfile("multiple_response_sets_missing_space_after_c");
-}
-
-#[test]
-fn multiple_response_sets_missing_space_after_e() {
- test_sack_sysfile("multiple_response_sets_missing_space_after_e");
-}
-
-#[test]
-fn multiple_response_sets_missing_label_source() {
- test_sack_sysfile("multiple_response_sets_missing_label_source");
-}
-
-#[test]
-fn multiple_response_sets_unexpected_label_source() {
- test_sack_sysfile("multiple_response_sets_unexpected_label_source");
-}
-
-#[test]
-fn multiple_response_sets_bad_counted_string() {
- test_sack_sysfile("multiple_response_sets_bad_counted_string");
-}
-
-#[test]
-fn multiple_response_sets_counted_string_missing_space() {
- test_sack_sysfile("multiple_response_sets_counted_string_missing_space");
-}
-
-#[test]
-fn multiple_response_sets_counted_string_bad_length() {
- test_sack_sysfile("multiple_response_sets_counted_string_bad_length");
-}
-
-#[test]
-fn multiple_response_sets_missing_space_after_counted_string() {
- test_sack_sysfile("multiple_response_sets_missing_space_after_counted_string");
-}
-
-#[test]
-fn multiple_response_sets_missing_newline_after_variable_name() {
- test_sack_sysfile("multiple_response_sets_missing_newline_after_variable_name");
-}
-
-#[test]
-fn multiple_response_sets_duplicate_variable_name() {
- test_sack_sysfile("multiple_response_sets_duplicate_variable_name");
-}
-
-#[test]
-fn mixed_variable_types_in_mrsets() {
- test_sack_sysfile("mixed_variable_types_in_mrsets");
-}
-
-#[test]
-fn missing_newline_after_variable_name_in_mrsets() {
- test_sack_sysfile("missing_newline_after_variable_name_in_mrsets");
-}
-
-#[test]
-fn zero_or_one_variable_in_mrset() {
- test_sack_sysfile("zero_or_one_variable_in_mrset");
-}
-
-#[test]
-fn wrong_display_parameter_size() {
- test_sack_sysfile("wrong_display_parameter_size");
-}
-
-#[test]
-fn wrong_display_parameter_count() {
- test_sack_sysfile("wrong_display_parameter_count");
-}
-
-#[test]
-fn wrong_display_measurement_level() {
- test_sack_sysfile("wrong_display_measurement_level");
-}
-
-#[test]
-fn wrong_display_alignment() {
- test_sack_sysfile("wrong_display_alignment");
-}
-
-#[test]
-fn bad_variable_name_in_variable_value_pair() {
- test_sack_sysfile("bad_variable_name_in_variable_value_pair");
-}
-
-#[test]
-fn duplicate_long_variable_name() {
- test_sack_sysfile("duplicate_long_variable_name");
-}
-
-#[test]
-fn bad_very_long_string_length() {
- test_sack_sysfile("bad_very_long_string_length");
-}
-
-#[test]
-fn bad_very_long_string_segment_width() {
- test_sack_sysfile("bad_very_long_string_segment_width");
-}
-
-#[test]
-fn too_many_value_labels() {
- test_sack_sysfile("too_many_value_labels");
-}
-
-#[test]
-fn missing_type_4_record() {
- test_sack_sysfile("missing_type_4_record");
-}
-
-#[test]
-fn value_label_with_no_associated_variables() {
- test_sack_sysfile("value_label_with_no_associated_variables");
-}
-
-#[test]
-fn type_4_record_names_long_string_variable() {
- test_sack_sysfile("type_4_record_names_long_string_variable");
-}
-
-#[test]
-fn value_label_variable_indexes_must_be_in_correct_range() {
- test_sack_sysfile("value_label_variable_indexes_must_be_in_correct_range");
-}
-
-#[test]
-fn value_label_variable_indexes_must_not_be_long_string_continuation() {
- test_sack_sysfile("value_label_variable_indexes_must_not_be_long_string_continuation");
-}
-
-#[test]
-fn variables_for_value_label_must_all_be_same_type() {
- test_sack_sysfile("variables_for_value_label_must_all_be_same_type");
-}
-
-#[test]
-fn duplicate_value_labels_type() {
- test_sack_sysfile("duplicate_value_labels_type");
-}
-
-#[test]
-fn missing_attribute_value() {
- test_sack_sysfile("missing_attribute_value");
-}
-
-#[test]
-fn unquoted_attribute_value() {
- test_sack_sysfile("unquoted_attribute_value");
-}
-
-#[test]
-fn duplicate_attribute_name() {
- test_sack_sysfile("duplicate_attribute_name");
-}
-
-#[test]
-fn bad_variable_name_in_long_string_value_label() {
- test_sack_sysfile("bad_variable_name_in_long_string_value_label");
-}
-
-#[test]
-fn fewer_data_records_than_indicated_by_file_header() {
- test_sack_sysfile("fewer_data_records_than_indicated_by_file_header");
-}
-
-#[test]
-fn more_data_records_than_indicated_by_file_header() {
- test_sack_sysfile("more_data_records_than_indicated_by_file_header");
-}
-
-#[test]
-fn partial_data_record_between_variables() {
- test_sack_sysfile("partial_data_record_between_variables");
-}
-
-#[test]
-fn partial_data_record_within_long_string() {
- test_sack_sysfile("partial_data_record_within_long_string");
-}
-
-#[test]
-fn partial_compressed_data_record() {
- test_sack_sysfile("partial_compressed_data_record");
-}
-
-#[test]
-fn zcompressed_data_bad_zheader_ofs() {
- test_sack_sysfile("zcompressed_data_bad_zheader_ofs");
-}
-
-#[test]
-fn zcompressed_data_bad_ztrailer_ofs() {
- test_sack_sysfile("zcompressed_data_bad_ztrailer_ofs");
-}
-
-#[test]
-fn zcompressed_data_invalid_ztrailer_len() {
- test_sack_sysfile("zcompressed_data_invalid_ztrailer_len");
-}
-
-#[test]
-fn zcompressed_data_wrong_ztrailer_len() {
- test_sack_sysfile("zcompressed_data_wrong_ztrailer_len");
-}
-
-#[test]
-fn zcompressed_data_wrong_ztrailer_bias() {
- test_sack_sysfile("zcompressed_data_wrong_ztrailer_bias");
-}
-
-#[test]
-fn zcompressed_data_wrong_ztrailer_zero() {
- test_sack_sysfile("zcompressed_data_wrong_ztrailer_zero");
-}
-
-#[test]
-fn zcompressed_data_wrong_block_size() {
- test_sack_sysfile("zcompressed_data_wrong_block_size");
-}
-
-#[test]
-fn zcompressed_data_wrong_n_blocks() {
- test_sack_sysfile("zcompressed_data_wrong_n_blocks");
-}
-
-#[test]
-fn zcompressed_data_wrong_uncompressed_ofs() {
- test_sack_sysfile("zcompressed_data_wrong_uncompressed_ofs");
-}
-
-#[test]
-fn zcompressed_data_wrong_compressed_ofs() {
- test_sack_sysfile("zcompressed_data_wrong_compressed_ofs");
-}
-
-#[test]
-fn zcompressed_data_compressed_sizes_dont_add_up() {
- test_sack_sysfile("zcompressed_data_compressed_sizes_dont_add_up");
-}
-
-#[test]
-fn zcompressed_data_uncompressed_size_block_size() {
- test_sack_sysfile("zcompressed_data_uncompressed_size_block_size");
-}
-
-#[test]
-fn zcompressed_data_compression_expands_data_too_much() {
- test_sack_sysfile("zcompressed_data_compression_expands_data_too_much");
-}
-
-#[test]
-fn zcompressed_data_compressed_sizes_don_t_add_up() {
- test_sack_sysfile("zcompressed_data_compressed_sizes_don_t_add_up");
-}
-
-/// CVE-2017-10791.
-/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467004.
-/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
-/// See also https://security-tracker.debian.org/tracker/CVE-2017-10791.
-/// Found by team OWL337, using the collAFL fuzzer.
-#[test]
-fn integer_overflows_in_long_string_missing_values() {
- test_raw_sysfile("integer_overflows_in_long_string_missing_values");
-}
-
-/// CVE-2017-10792.
-/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467005.
-/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
-/// See also https://security-tracker.debian.org/tracker/CVE-2017-10792.
-/// Reported by team OWL337, with fuzzer collAFL.
-#[test]
-fn null_dereference_skipping_bad_extension_record_18() {
- test_raw_sysfile("null_dereference_skipping_bad_extension_record_18");
-}
-
-/// Duplicate variable name handling negative test.
-///
-/// SPSS-generated system file can contain duplicate variable names (see bug
-/// #41475).
-#[test]
-fn duplicate_variable_name() {
- test_sack_sysfile("duplicate_variable_name");
-}
-
-#[test]
-fn encrypted_file() {
- test_encrypted_sysfile("test-encrypted.sav", "pspp");
-}
-
-#[test]
-fn encrypted_file_without_password() {
- let error = ReadOptions::new(|_| {
- panic!();
- })
- .open_file("src/crypto/testdata/test-encrypted.sav")
- .unwrap_err();
- assert!(matches!(
- error.downcast::<raw::Error>().unwrap().details,
- ErrorDetails::Encrypted
- ));
-}
-
-/// Tests the most basic kind of writing a system file, just writing a few
-/// numeric variables and cases.
-fn write_numeric(compression: Option<Compression>, compression_string: &str) {
- let mut dictionary = Dictionary::new(UTF_8);
- for i in 0..4 {
- let name = Identifier::new(format!("variable{i}")).unwrap();
- dictionary
- .add_var(Variable::new(name, VarWidth::Numeric, UTF_8))
- .unwrap();
- }
- let mut cases = WriteOptions::reproducible(compression)
- .write_writer(&dictionary, Cursor::new(Vec::new()))
- .unwrap();
- for case in [
- [1, 1, 1, 2],
- [1, 1, 2, 30],
- [1, 2, 1, 8],
- [1, 2, 2, 20],
- [2, 1, 1, 2],
- [2, 1, 2, 22],
- [2, 2, 1, 1],
- [2, 2, 2, 3],
- ] {
- cases
- .write_case(
- case.into_iter()
- .map(|number| Datum::<&str>::Number(Some(number as f64))),
- )
- .unwrap();
- }
- let sysfile = cases.finish().unwrap().unwrap().into_inner();
- let expected_filename = PathBuf::from(&format!(
- "src/sys/testdata/write-numeric-{compression_string}.expected"
- ));
- let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
- test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
-}
-
-#[test]
-fn write_numeric_uncompressed() {
- write_numeric(None, "uncompressed");
-}
-
-#[test]
-fn write_numeric_simple() {
- write_numeric(Some(Compression::Simple), "simple");
-}
-
-#[test]
-fn write_numeric_zlib() {
- write_numeric(Some(Compression::ZLib), "zlib");
-}
-
-/// Tests writing string data.
-fn write_string(compression: Option<Compression>, compression_string: &str) {
- let mut dictionary = Dictionary::new(UTF_8);
- dictionary
- .add_var(Variable::new(
- Identifier::new("s1").unwrap(),
- VarWidth::String(1),
- UTF_8,
- ))
- .unwrap();
-
- dictionary
- .add_var(Variable::new(
- Identifier::new("s2").unwrap(),
- VarWidth::String(2),
- UTF_8,
- ))
- .unwrap();
-
- dictionary
- .add_var(Variable::new(
- Identifier::new("s3").unwrap(),
- VarWidth::String(3),
- UTF_8,
- ))
- .unwrap();
-
- dictionary
- .add_var(Variable::new(
- Identifier::new("s4").unwrap(),
- VarWidth::String(9),
- UTF_8,
- ))
- .unwrap();
-
- dictionary
- .add_var(Variable::new(
- Identifier::new("s566").unwrap(),
- VarWidth::String(566),
- UTF_8,
- ))
- .unwrap();
-
- let mut cases = WriteOptions::reproducible(compression)
- .write_writer(&dictionary, Cursor::new(Vec::new()))
- .unwrap();
- for case in [
- ["1", "1", "1", "xyzzyquux", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n"],
- ["1", "2", "1", "8", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],
- ] {
- cases
- .write_case(case.into_iter().map(|s| Datum::String(s)))
- .unwrap();
- }
- let sysfile = cases.finish().unwrap().unwrap().into_inner();
- let expected_filename = PathBuf::from(&format!(
- "src/sys/testdata/write-string-{compression_string}.expected"
- ));
- let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
- test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
-}
-
-#[test]
-fn write_string_uncompressed() {
- write_string(None, "uncompressed");
-}
-
-#[test]
-fn write_string_simple() {
- write_string(Some(Compression::Simple), "simple");
-}
-
-#[test]
-fn write_string_zlib() {
- write_string(Some(Compression::ZLib), "zlib");
-}
-
-fn test_raw_sysfile(name: &str) {
- let input_filename = Path::new("src/sys/testdata")
- .join(name)
- .with_extension("sav");
- let sysfile = BufReader::new(File::open(&input_filename).unwrap());
- let expected_filename = input_filename.with_extension("expected");
- let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
- test_sysfile(sysfile, &expected, &expected_filename);
-}
-
-fn test_encrypted_sysfile(name: &str, password: &str) {
- let input_filename = Path::new("src/sys/testdata")
- .join(name)
- .with_extension("sav");
- let sysfile = EncryptedFile::new(File::open(&input_filename).unwrap())
- .unwrap()
- .unlock(password.as_bytes())
- .unwrap();
- let expected_filename = input_filename.with_extension("expected");
- let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
- test_sysfile(sysfile, &expected, &expected_filename);
-}
-
-fn test_sack_sysfile(name: &str) {
- let input_filename = Path::new("src/sys/testdata")
- .join(name)
- .with_extension("sack");
- let input = String::from_utf8(std::fs::read(&input_filename).unwrap()).unwrap();
- let expected_filename = input_filename.with_extension("expected");
- let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
- for endian in [Endian::Big, Endian::Little] {
- let expected = expected.replace(
- "{endian}",
- match endian {
- Endian::Big => "1",
- Endian::Little => "2",
- },
- );
- let sysfile = sack(&input, Some(&input_filename), endian).unwrap();
- test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
- }
-}
-
-fn test_sysfile<R>(sysfile: R, expected: &str, expected_filename: &Path)
-where
- R: BufRead + Seek + 'static,
-{
- let mut warnings = Vec::new();
- let output = match ReadOptions::new(|warning| warnings.push(warning)).open_reader(sysfile) {
- Ok(system_file) => {
- let (dictionary, metadata, cases) = system_file.into_parts();
-
- let mut output = Vec::new();
- output.extend(
- warnings
- .into_iter()
- .map(|warning| Item::from(Text::new_log(warning.to_string()))),
- );
- output.push(PivotTable::from(&metadata).into());
- output.extend(dictionary.all_pivot_tables().into_iter().map_into());
- let variables =
- Group::new("Variable").with_multiple(dictionary.variables.iter().map(|var| &**var));
- let mut case_numbers = Group::new("Case").with_label_shown();
- let mut data = Vec::new();
- for case in cases {
- match case {
- Ok(case) => {
- case_numbers
- .push(Value::new_integer(Some((case_numbers.len() + 1) as f64)));
- data.push(
- case.into_iter()
- .map(|datum| Value::new_datum(&datum))
- .collect::<Vec<_>>(),
- );
- }
- Err(error) => {
- output.push(Item::from(Text::new_log(error.to_string())));
- }
- }
- }
- if !data.is_empty() {
- let mut pt = PivotTable::new([
- (Axis3::X, Dimension::new(variables)),
- (Axis3::Y, Dimension::new(case_numbers)),
- ]);
- for (row_number, row) in data.into_iter().enumerate() {
- for (column_number, datum) in row.into_iter().enumerate() {
- pt.insert(&[column_number, row_number], datum);
- }
- }
- output.push(pt.into());
- }
- Item::new(Details::Group(output.into_iter().map(Arc::new).collect()))
- }
- Err(error) => Item::new(Details::Text(Box::new(Text::new_log(error.to_string())))),
- };
-
- let actual = output.to_string();
- if expected != actual {
- if std::env::var("PSPP_REFRESH_EXPECTED").is_ok() {
- std::fs::write(expected_filename, actual).unwrap();
- panic!("{}: refreshed output", expected_filename.display());
- } else {
- eprintln!("note: rerun with PSPP_REFRESH_EXPECTED=1 to refresh expected output");
- }
- }
- assert_lines_eq(&expected, expected_filename.display(), &actual, "actual");
-}
--- /dev/null
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see <http://www.gnu.org/licenses/>.
+
+use std::{
+ fs::File,
+ io::{BufRead, BufReader, Cursor, Seek},
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use binrw::Endian;
+use encoding_rs::UTF_8;
+use itertools::Itertools;
+
+use crate::{
+ crypto::EncryptedFile,
+ data::Datum,
+ dictionary::Dictionary,
+ identifier::Identifier,
+ output::{
+ pivot::{tests::assert_lines_eq, Axis3, Dimension, Group, PivotTable, Value},
+ Details, Item, Text,
+ },
+ sys::{
+ cooked::ReadOptions,
+ raw::{self, records::Compression, ErrorDetails},
+ sack::sack,
+ WriteOptions,
+ },
+ variable::{VarWidth, Variable},
+};
+
+#[test]
+fn variable_labels_and_missing_values() {
+ test_sack_sysfile("variable_labels_and_missing_values");
+}
+
+#[test]
+fn unspecified_number_of_variable_positions() {
+ test_sack_sysfile("unspecified_number_of_variable_positions");
+}
+
+#[test]
+fn wrong_variable_positions_but_v13() {
+ test_sack_sysfile("wrong_variable_positions_but_v13");
+}
+
+#[test]
+fn value_labels() {
+ test_sack_sysfile("value_labels");
+}
+
+#[test]
+fn documents() {
+ test_sack_sysfile("documents");
+}
+
+#[test]
+fn empty_document_record() {
+ test_sack_sysfile("empty_document_record");
+}
+
+#[test]
+fn variable_sets() {
+ test_sack_sysfile("variable_sets");
+}
+
+#[test]
+fn multiple_response_sets() {
+ test_sack_sysfile("multiple_response_sets");
+}
+
+#[test]
+fn extra_product_info() {
+ // Also checks for handling of CR-only line ends in file label and extra
+ // product info.
+ test_sack_sysfile("extra_product_info");
+}
+
+#[test]
+fn variable_display_without_width() {
+ test_sack_sysfile("variable_display_without_width");
+}
+
+#[test]
+fn variable_display_with_width() {
+ test_sack_sysfile("variable_display_with_width");
+}
+
+#[test]
+fn long_variable_names() {
+ test_sack_sysfile("long_variable_names");
+}
+
+#[test]
+fn very_long_strings() {
+ test_sack_sysfile("very_long_strings");
+}
+
+#[test]
+fn attributes() {
+ test_sack_sysfile("attributes");
+}
+
+#[test]
+fn variable_roles() {
+ test_sack_sysfile("variable_roles");
+}
+
+#[test]
+fn compressed_data() {
+ test_sack_sysfile("compressed_data");
+}
+
+#[test]
+fn compressed_data_zero_bias() {
+ test_sack_sysfile("compressed_data_zero_bias");
+}
+
+#[test]
+fn compressed_data_other_bias() {
+ test_sack_sysfile("compressed_data_other_bias");
+}
+
+#[test]
+fn zcompressed_data() {
+ test_sack_sysfile("zcompressed_data");
+}
+
+#[test]
+fn no_variables() {
+ test_sack_sysfile("no_variables");
+}
+
+#[test]
+fn unknown_encoding() {
+ test_sack_sysfile("unknown_encoding");
+}
+
+#[test]
+fn misplaced_type_4_record() {
+ test_sack_sysfile("misplaced_type_4_record");
+}
+
+#[test]
+fn bad_record_type() {
+ test_sack_sysfile("bad_record_type");
+}
+
+#[test]
+fn wrong_variable_positions() {
+ test_sack_sysfile("wrong_variable_positions");
+}
+
+#[test]
+fn invalid_variable_name() {
+ test_sack_sysfile("invalid_variable_name");
+}
+
+#[test]
+fn invalid_label_indicator() {
+ test_sack_sysfile("invalid_label_indicator");
+}
+
+#[test]
+fn invalid_missing_indicator() {
+ test_sack_sysfile("invalid_missing_indicator");
+}
+
+#[test]
+fn invalid_missing_indicator2() {
+ test_sack_sysfile("invalid_missing_indicator2");
+}
+
+#[test]
+fn missing_string_continuation() {
+ test_sack_sysfile("missing_string_continuation");
+}
+
+#[test]
+fn invalid_variable_format() {
+ test_sack_sysfile("invalid_variable_format");
+}
+
+#[test]
+fn invalid_long_string_missing_values() {
+ test_sack_sysfile("invalid_long_string_missing_values");
+}
+
+#[test]
+fn weight_must_be_numeric() {
+ test_sack_sysfile("weight_must_be_numeric");
+}
+
+#[test]
+fn weight_variable_bad_index() {
+ test_sack_sysfile("weight_variable_bad_index");
+}
+
+#[test]
+fn weight_variable_continuation() {
+ test_sack_sysfile("weight_variable_continuation");
+}
+
+#[test]
+fn multiple_documents_records() {
+ test_sack_sysfile("multiple_documents_records");
+}
+
+#[test]
+fn unknown_extension_record() {
+ test_sack_sysfile("unknown_extension_record");
+}
+
+#[test]
+fn extension_too_large() {
+ test_sack_sysfile("extension_too_large");
+}
+
+#[test]
+fn bad_machine_integer_info_count() {
+ test_sack_sysfile("bad_machine_integer_info_count");
+}
+
+#[test]
+fn bad_machine_integer_info_float_format() {
+ test_sack_sysfile("bad_machine_integer_info_float_format");
+}
+
+#[test]
+fn bad_machine_integer_info_endianness() {
+ test_sack_sysfile("bad_machine_integer_info_endianness");
+}
+
+#[test]
+fn bad_machine_float_info_size() {
+ test_sack_sysfile("bad_machine_float_info_size");
+}
+
+#[test]
+fn wrong_special_floats() {
+ test_sack_sysfile("wrong_special_floats");
+}
+
+#[test]
+fn variable_sets_unknown_variable() {
+ test_sack_sysfile("variable_sets_unknown_variable");
+}
+
+#[test]
+fn multiple_response_sets_bad_name() {
+ test_sack_sysfile("multiple_response_sets_bad_name");
+}
+
+#[test]
+fn multiple_response_sets_missing_space_after_c() {
+ test_sack_sysfile("multiple_response_sets_missing_space_after_c");
+}
+
+#[test]
+fn multiple_response_sets_missing_space_after_e() {
+ test_sack_sysfile("multiple_response_sets_missing_space_after_e");
+}
+
+#[test]
+fn multiple_response_sets_missing_label_source() {
+ test_sack_sysfile("multiple_response_sets_missing_label_source");
+}
+
+#[test]
+fn multiple_response_sets_unexpected_label_source() {
+ test_sack_sysfile("multiple_response_sets_unexpected_label_source");
+}
+
+#[test]
+fn multiple_response_sets_bad_counted_string() {
+ test_sack_sysfile("multiple_response_sets_bad_counted_string");
+}
+
+#[test]
+fn multiple_response_sets_counted_string_missing_space() {
+ test_sack_sysfile("multiple_response_sets_counted_string_missing_space");
+}
+
+#[test]
+fn multiple_response_sets_counted_string_bad_length() {
+ test_sack_sysfile("multiple_response_sets_counted_string_bad_length");
+}
+
+#[test]
+fn multiple_response_sets_missing_space_after_counted_string() {
+ test_sack_sysfile("multiple_response_sets_missing_space_after_counted_string");
+}
+
+#[test]
+fn multiple_response_sets_missing_newline_after_variable_name() {
+ test_sack_sysfile("multiple_response_sets_missing_newline_after_variable_name");
+}
+
+#[test]
+fn multiple_response_sets_duplicate_variable_name() {
+ test_sack_sysfile("multiple_response_sets_duplicate_variable_name");
+}
+
+#[test]
+fn mixed_variable_types_in_mrsets() {
+ test_sack_sysfile("mixed_variable_types_in_mrsets");
+}
+
+#[test]
+fn missing_newline_after_variable_name_in_mrsets() {
+ test_sack_sysfile("missing_newline_after_variable_name_in_mrsets");
+}
+
+#[test]
+fn zero_or_one_variable_in_mrset() {
+ test_sack_sysfile("zero_or_one_variable_in_mrset");
+}
+
+#[test]
+fn wrong_display_parameter_size() {
+ test_sack_sysfile("wrong_display_parameter_size");
+}
+
+#[test]
+fn wrong_display_parameter_count() {
+ test_sack_sysfile("wrong_display_parameter_count");
+}
+
+#[test]
+fn wrong_display_measurement_level() {
+ test_sack_sysfile("wrong_display_measurement_level");
+}
+
+#[test]
+fn wrong_display_alignment() {
+ test_sack_sysfile("wrong_display_alignment");
+}
+
+#[test]
+fn bad_variable_name_in_variable_value_pair() {
+ test_sack_sysfile("bad_variable_name_in_variable_value_pair");
+}
+
+#[test]
+fn duplicate_long_variable_name() {
+ test_sack_sysfile("duplicate_long_variable_name");
+}
+
+#[test]
+fn bad_very_long_string_length() {
+ test_sack_sysfile("bad_very_long_string_length");
+}
+
+#[test]
+fn bad_very_long_string_segment_width() {
+ test_sack_sysfile("bad_very_long_string_segment_width");
+}
+
+#[test]
+fn too_many_value_labels() {
+ test_sack_sysfile("too_many_value_labels");
+}
+
+#[test]
+fn missing_type_4_record() {
+ test_sack_sysfile("missing_type_4_record");
+}
+
+#[test]
+fn value_label_with_no_associated_variables() {
+ test_sack_sysfile("value_label_with_no_associated_variables");
+}
+
+#[test]
+fn type_4_record_names_long_string_variable() {
+ test_sack_sysfile("type_4_record_names_long_string_variable");
+}
+
+#[test]
+fn value_label_variable_indexes_must_be_in_correct_range() {
+ test_sack_sysfile("value_label_variable_indexes_must_be_in_correct_range");
+}
+
+#[test]
+fn value_label_variable_indexes_must_not_be_long_string_continuation() {
+ test_sack_sysfile("value_label_variable_indexes_must_not_be_long_string_continuation");
+}
+
+#[test]
+fn variables_for_value_label_must_all_be_same_type() {
+ test_sack_sysfile("variables_for_value_label_must_all_be_same_type");
+}
+
+#[test]
+fn duplicate_value_labels_type() {
+ test_sack_sysfile("duplicate_value_labels_type");
+}
+
+#[test]
+fn missing_attribute_value() {
+ test_sack_sysfile("missing_attribute_value");
+}
+
+#[test]
+fn unquoted_attribute_value() {
+ test_sack_sysfile("unquoted_attribute_value");
+}
+
+#[test]
+fn duplicate_attribute_name() {
+ test_sack_sysfile("duplicate_attribute_name");
+}
+
+#[test]
+fn bad_variable_name_in_long_string_value_label() {
+ test_sack_sysfile("bad_variable_name_in_long_string_value_label");
+}
+
+#[test]
+fn fewer_data_records_than_indicated_by_file_header() {
+ test_sack_sysfile("fewer_data_records_than_indicated_by_file_header");
+}
+
+#[test]
+fn more_data_records_than_indicated_by_file_header() {
+ test_sack_sysfile("more_data_records_than_indicated_by_file_header");
+}
+
+#[test]
+fn partial_data_record_between_variables() {
+ test_sack_sysfile("partial_data_record_between_variables");
+}
+
+#[test]
+fn partial_data_record_within_long_string() {
+ test_sack_sysfile("partial_data_record_within_long_string");
+}
+
+#[test]
+fn partial_compressed_data_record() {
+ test_sack_sysfile("partial_compressed_data_record");
+}
+
+#[test]
+fn zcompressed_data_bad_zheader_ofs() {
+ test_sack_sysfile("zcompressed_data_bad_zheader_ofs");
+}
+
+#[test]
+fn zcompressed_data_bad_ztrailer_ofs() {
+ test_sack_sysfile("zcompressed_data_bad_ztrailer_ofs");
+}
+
+#[test]
+fn zcompressed_data_invalid_ztrailer_len() {
+ test_sack_sysfile("zcompressed_data_invalid_ztrailer_len");
+}
+
+#[test]
+fn zcompressed_data_wrong_ztrailer_len() {
+ test_sack_sysfile("zcompressed_data_wrong_ztrailer_len");
+}
+
+#[test]
+fn zcompressed_data_wrong_ztrailer_bias() {
+ test_sack_sysfile("zcompressed_data_wrong_ztrailer_bias");
+}
+
+#[test]
+fn zcompressed_data_wrong_ztrailer_zero() {
+ test_sack_sysfile("zcompressed_data_wrong_ztrailer_zero");
+}
+
+#[test]
+fn zcompressed_data_wrong_block_size() {
+ test_sack_sysfile("zcompressed_data_wrong_block_size");
+}
+
+#[test]
+fn zcompressed_data_wrong_n_blocks() {
+ test_sack_sysfile("zcompressed_data_wrong_n_blocks");
+}
+
+#[test]
+fn zcompressed_data_wrong_uncompressed_ofs() {
+ test_sack_sysfile("zcompressed_data_wrong_uncompressed_ofs");
+}
+
+#[test]
+fn zcompressed_data_wrong_compressed_ofs() {
+ test_sack_sysfile("zcompressed_data_wrong_compressed_ofs");
+}
+
+#[test]
+fn zcompressed_data_compressed_sizes_dont_add_up() {
+ test_sack_sysfile("zcompressed_data_compressed_sizes_dont_add_up");
+}
+
+#[test]
+fn zcompressed_data_uncompressed_size_block_size() {
+ test_sack_sysfile("zcompressed_data_uncompressed_size_block_size");
+}
+
+#[test]
+fn zcompressed_data_compression_expands_data_too_much() {
+ test_sack_sysfile("zcompressed_data_compression_expands_data_too_much");
+}
+
+#[test]
+fn zcompressed_data_compressed_sizes_don_t_add_up() {
+ test_sack_sysfile("zcompressed_data_compressed_sizes_don_t_add_up");
+}
+
+/// CVE-2017-10791.
+/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467004.
+/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
+/// See also https://security-tracker.debian.org/tracker/CVE-2017-10791.
+/// Found by team OWL337, using the collAFL fuzzer.
+#[test]
+fn integer_overflows_in_long_string_missing_values() {
+ test_raw_sysfile("integer_overflows_in_long_string_missing_values");
+}
+
+/// CVE-2017-10792.
+/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467005.
+/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
+/// See also https://security-tracker.debian.org/tracker/CVE-2017-10792.
+/// Reported by team OWL337, with fuzzer collAFL.
+#[test]
+fn null_dereference_skipping_bad_extension_record_18() {
+ test_raw_sysfile("null_dereference_skipping_bad_extension_record_18");
+}
+
+/// Duplicate variable name handling negative test.
+///
+/// SPSS-generated system file can contain duplicate variable names (see bug
+/// #41475).
+#[test]
+fn duplicate_variable_name() {
+ test_sack_sysfile("duplicate_variable_name");
+}
+
+#[test]
+fn encrypted_file() {
+ test_encrypted_sysfile("test-encrypted.sav", "pspp");
+}
+
+#[test]
+fn encrypted_file_without_password() {
+ let error = ReadOptions::new(|_| {
+ panic!();
+ })
+ .open_file("src/crypto/testdata/test-encrypted.sav")
+ .unwrap_err();
+ assert!(matches!(
+ error.downcast::<raw::Error>().unwrap().details,
+ ErrorDetails::Encrypted
+ ));
+}
+
+/// Tests the most basic kind of writing a system file, just writing a few
+/// numeric variables and cases.
+fn write_numeric(compression: Option<Compression>, compression_string: &str) {
+ let mut dictionary = Dictionary::new(UTF_8);
+ for i in 0..4 {
+ let name = Identifier::new(format!("variable{i}")).unwrap();
+ dictionary
+ .add_var(Variable::new(name, VarWidth::Numeric, UTF_8))
+ .unwrap();
+ }
+ let mut cases = WriteOptions::reproducible(compression)
+ .write_writer(&dictionary, Cursor::new(Vec::new()))
+ .unwrap();
+ for case in [
+ [1, 1, 1, 2],
+ [1, 1, 2, 30],
+ [1, 2, 1, 8],
+ [1, 2, 2, 20],
+ [2, 1, 1, 2],
+ [2, 1, 2, 22],
+ [2, 2, 1, 1],
+ [2, 2, 2, 3],
+ ] {
+ cases
+ .write_case(
+ case.into_iter()
+ .map(|number| Datum::<&str>::Number(Some(number as f64))),
+ )
+ .unwrap();
+ }
+ let sysfile = cases.finish().unwrap().unwrap().into_inner();
+ let expected_filename = PathBuf::from(&format!(
+ "src/sys/testdata/write-numeric-{compression_string}.expected"
+ ));
+ let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+ test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
+}
+
+#[test]
+fn write_numeric_uncompressed() {
+ write_numeric(None, "uncompressed");
+}
+
+#[test]
+fn write_numeric_simple() {
+ write_numeric(Some(Compression::Simple), "simple");
+}
+
+#[test]
+fn write_numeric_zlib() {
+ write_numeric(Some(Compression::ZLib), "zlib");
+}
+
+/// Tests writing string data.
+fn write_string(compression: Option<Compression>, compression_string: &str) {
+ let mut dictionary = Dictionary::new(UTF_8);
+ dictionary
+ .add_var(Variable::new(
+ Identifier::new("s1").unwrap(),
+ VarWidth::String(1),
+ UTF_8,
+ ))
+ .unwrap();
+
+ dictionary
+ .add_var(Variable::new(
+ Identifier::new("s2").unwrap(),
+ VarWidth::String(2),
+ UTF_8,
+ ))
+ .unwrap();
+
+ dictionary
+ .add_var(Variable::new(
+ Identifier::new("s3").unwrap(),
+ VarWidth::String(3),
+ UTF_8,
+ ))
+ .unwrap();
+
+ dictionary
+ .add_var(Variable::new(
+ Identifier::new("s4").unwrap(),
+ VarWidth::String(9),
+ UTF_8,
+ ))
+ .unwrap();
+
+ dictionary
+ .add_var(Variable::new(
+ Identifier::new("s566").unwrap(),
+ VarWidth::String(566),
+ UTF_8,
+ ))
+ .unwrap();
+
+ let mut cases = WriteOptions::reproducible(compression)
+ .write_writer(&dictionary, Cursor::new(Vec::new()))
+ .unwrap();
+ for case in [
+ ["1", "1", "1", "xyzzyquux", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n"],
+ ["1", "2", "1", "8", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],
+ ] {
+ cases
+ .write_case(case.into_iter().map(|s| Datum::String(s)))
+ .unwrap();
+ }
+ let sysfile = cases.finish().unwrap().unwrap().into_inner();
+ let expected_filename = PathBuf::from(&format!(
+ "src/sys/testdata/write-string-{compression_string}.expected"
+ ));
+ let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+ test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
+}
+
+#[test]
+fn write_string_uncompressed() {
+ write_string(None, "uncompressed");
+}
+
+#[test]
+fn write_string_simple() {
+ write_string(Some(Compression::Simple), "simple");
+}
+
+#[test]
+fn write_string_zlib() {
+ write_string(Some(Compression::ZLib), "zlib");
+}
+
+fn test_raw_sysfile(name: &str) {
+ let input_filename = Path::new("src/sys/testdata")
+ .join(name)
+ .with_extension("sav");
+ let sysfile = BufReader::new(File::open(&input_filename).unwrap());
+ let expected_filename = input_filename.with_extension("expected");
+ let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+ test_sysfile(sysfile, &expected, &expected_filename);
+}
+
+fn test_encrypted_sysfile(name: &str, password: &str) {
+ let input_filename = Path::new("src/sys/testdata")
+ .join(name)
+ .with_extension("sav");
+ let sysfile = EncryptedFile::new(File::open(&input_filename).unwrap())
+ .unwrap()
+ .unlock(password.as_bytes())
+ .unwrap();
+ let expected_filename = input_filename.with_extension("expected");
+ let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+ test_sysfile(sysfile, &expected, &expected_filename);
+}
+
+fn test_sack_sysfile(name: &str) {
+ let input_filename = Path::new("src/sys/testdata")
+ .join(name)
+ .with_extension("sack");
+ let input = String::from_utf8(std::fs::read(&input_filename).unwrap()).unwrap();
+ let expected_filename = input_filename.with_extension("expected");
+ let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+ for endian in [Endian::Big, Endian::Little] {
+ let expected = expected.replace(
+ "{endian}",
+ match endian {
+ Endian::Big => "1",
+ Endian::Little => "2",
+ },
+ );
+ let sysfile = sack(&input, Some(&input_filename), endian).unwrap();
+ test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
+ }
+}
+
+fn test_sysfile<R>(sysfile: R, expected: &str, expected_filename: &Path)
+where
+ R: BufRead + Seek + 'static,
+{
+ let mut warnings = Vec::new();
+ let output = match ReadOptions::new(|warning| warnings.push(warning)).open_reader(sysfile) {
+ Ok(system_file) => {
+ let (dictionary, metadata, cases) = system_file.into_parts();
+
+ let mut output = Vec::new();
+ output.extend(
+ warnings
+ .into_iter()
+ .map(|warning| Item::from(Text::new_log(warning.to_string()))),
+ );
+ output.push(PivotTable::from(&metadata).into());
+ output.extend(dictionary.all_pivot_tables().into_iter().map_into());
+ let variables =
+ Group::new("Variable").with_multiple(dictionary.variables.iter().map(|var| &**var));
+ let mut case_numbers = Group::new("Case").with_label_shown();
+ let mut data = Vec::new();
+ for case in cases {
+ match case {
+ Ok(case) => {
+ case_numbers
+ .push(Value::new_integer(Some((case_numbers.len() + 1) as f64)));
+ data.push(
+ case.into_iter()
+ .map(|datum| Value::new_datum(&datum))
+ .collect::<Vec<_>>(),
+ );
+ }
+ Err(error) => {
+ output.push(Item::from(Text::new_log(error.to_string())));
+ }
+ }
+ }
+ if !data.is_empty() {
+ let mut pt = PivotTable::new([
+ (Axis3::X, Dimension::new(variables)),
+ (Axis3::Y, Dimension::new(case_numbers)),
+ ]);
+ for (row_number, row) in data.into_iter().enumerate() {
+ for (column_number, datum) in row.into_iter().enumerate() {
+ pt.insert(&[column_number, row_number], datum);
+ }
+ }
+ output.push(pt.into());
+ }
+ Item::new(Details::Group(output.into_iter().map(Arc::new).collect()))
+ }
+ Err(error) => Item::new(Details::Text(Box::new(Text::new_log(error.to_string())))),
+ };
+
+ let actual = output.to_string();
+ if expected != actual {
+ if std::env::var("PSPP_REFRESH_EXPECTED").is_ok() {
+ std::fs::write(expected_filename, actual).unwrap();
+ panic!("{}: refreshed output", expected_filename.display());
+ } else {
+ eprintln!("note: rerun with PSPP_REFRESH_EXPECTED=1 to refresh expected output");
+ }
+ }
+ assert_lines_eq(&expected, expected_filename.display(), &actual, "actual");
+}