From 2c0447d567b316175532749b8ab2606857ad43b6 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Fri, 9 Jan 2026 09:28:23 -0800 Subject: [PATCH] work --- rust/doc/src/spv/light-detail.md | 16 +++ rust/pspp/src/output/pivot.rs | 5 + rust/pspp/src/output/pivot/value.rs | 153 ++++++++++++++++------------ rust/pspp/src/spv/read/light.rs | 1 - rust/pspp/src/spv/read/tests.rs | 6 ++ 5 files changed, 114 insertions(+), 67 deletions(-) diff --git a/rust/doc/src/spv/light-detail.md b/rust/doc/src/spv/light-detail.md index 554a9b83ef..2eadec547c 100644 --- a/rust/doc/src/spv/light-detail.md +++ b/rust/doc/src/spv/light-detail.md @@ -1067,6 +1067,22 @@ the first nonzero byte in the encoding. * `[%1:, ^1:]1` Given appropriate values, expands to `1, 2, 3`. + * `[A:B:C]I` + This extends the previous form so that the first values are + expanded using `A`, the last values are expanded using `C`, and + any of them in the middle are expanded using `B`. (If there are + only enough values to expand once, instead of just using `A`, the + entire expansion is dropped, which suggests that this form is only + used if there is more than one set of expansion values.) + + Within `A`, conversions are written `%J`; within `B`, as `^J`; + within `C`, as `$J`. An example from the corpus: + + * `[%1:, ^1: and $1]1` + Expands to ` and ` for two arguments, `, and ` + for three arguments, `, , , and ` for four, and so + on. + The template string is localized to the user's locale. A writer may safely omit all of the optional `00` bytes at the diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 933ab3473f..70f63afd19 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -2131,6 +2131,9 @@ mod test { ("4: [%1:, ^1:]1", "4: First, 1.00, Second, 2"), ("5: [%1 = %2:, ^1 = ^2:]1", "5: First = 1.00, Second = 2"), ("6: [%1:, ^1:]1", "6: First, 1.00, Second, 2"), + ("7: [%1:, ^1: and $1]1", "7: First, 1.00, Second and 2"), + ("8: [%1:, ^1: and $1]3", "8: One and two"), + ("9: [%1:, ^1: and $1]4", "9: Just one"), ] { let value = Value::new(ValueInner::Template(TemplateValue { args: vec![ @@ -2146,6 +2149,8 @@ mod test { Value::new_user_text("Fourth"), Value::new_integer(Some(4.0)), ], + vec![Value::new_user_text("One"), Value::new_user_text("two")], + vec![Value::new_user_text("Just one")], ], localized: String::from(template), id: None, diff --git a/rust/pspp/src/output/pivot/value.rs b/rust/pspp/src/output/pivot/value.rs index fe88f63c50..263167b61a 100644 --- a/rust/pspp/src/output/pivot/value.rs +++ b/rust/pspp/src/output/pivot/value.rs @@ -984,15 +984,77 @@ impl TemplateValue { display: &DisplayValue<'a>, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - fn extract_inner_template(input: &str) -> (&str, &str) { - let mut prev = None; + dbg!(self); + + #[derive(Copy, Clone, Debug)] + struct InnerTemplate<'b> { + template: &'b str, + escape: char, + } + + impl<'b> InnerTemplate<'b> { + fn new(template: &'b str, escape: char) -> Self { + Self { template, escape } + } + + fn extract(input: &'b str, escape: char, end: char) -> (Self, &'b str) { + let mut prev = None; + for (index, c) in input.char_indices() { + if c == end && prev != Some('\\') { + return (Self::new(&input[..index], escape), &input[index + 1..]); + } + prev = Some(c); + } + (Self::new(input, escape), "") + } + + fn expand( + &self, + options: &ValueOptions, + f: &mut std::fmt::Formatter<'_>, + args: &mut std::slice::Iter, + ) -> Result { + let mut iter = self.template.chars(); + + // Always consume at least 1 argument to avoid infinite loop. + let mut args_consumed = 1; + + while let Some(c) = iter.next() { + match c { + '\\' => { + let c = iter.next().unwrap_or('\\') as char; + let c = if c == 'n' { '\n' } else { c }; + write!(f, "{c}")?; + } + c if c == self.escape => { + let (index, rest) = consume_int(iter.as_str()); + iter = rest.chars(); + if let Some(index) = index.checked_sub(1) + && let Some(arg) = args.as_slice().get(index) + { + args_consumed = args_consumed.max(index + 1); + write!(f, "{}", arg.display(options))?; + } + } + c => write!(f, "{c}")?, + } + } + for _ in 0..args_consumed { + args.next(); + } + Ok(args_consumed) + } + } + + fn consume_int(input: &str) -> (usize, &str) { + let mut n = 0; for (index, c) in input.char_indices() { - if c == ':' && prev != Some('\\') { - return (&input[..index], &input[index + 1..]); + match c.to_digit(10) { + Some(digit) => n = n * 10 + digit as usize, + None => return (n, &input[index..]), } - prev = Some(c); } - (input, "") + (n, "") } // Arguments are formatted without leading zeros for `PCT` and `DOLLAR`. @@ -1012,7 +1074,7 @@ impl TemplateValue { f.write_char(c)?; } '^' => { - let (index, rest) = Self::consume_int(iter.as_str()); + let (index, rest) = consume_int(iter.as_str()); if let Some(index) = index.checked_sub(1) && let Some(arg) = self.args.get(index) && let Some(arg) = arg.first() @@ -1022,26 +1084,29 @@ impl TemplateValue { iter = rest.chars(); } '[' => { - let (a, rest) = extract_inner_template(iter.as_str()); - let (b, rest) = extract_inner_template(rest); - let rest = rest.strip_prefix("]").unwrap_or(rest); - let (index, rest) = Self::consume_int(rest); + let (a, rest) = InnerTemplate::extract(iter.as_str(), '%', ':'); + let (b, rest) = InnerTemplate::extract(rest, '^', ':'); + let (c, rest) = InnerTemplate::extract(rest, '$', ']'); + let (index, rest) = consume_int(rest); iter = rest.chars(); + let (first, mid, last) = if a.template.is_empty() { + (b, b, b) + } else if c.template.is_empty() { + (a, b, b) + } else { + (a, b, c) + }; if let Some(index) = index.checked_sub(1) && let Some(args) = self.args.get(index) { - let mut args = args.as_slice(); - let (mut template, mut escape) = - if !a.is_empty() { (a, '%') } else { (b, '^') }; - while !args.is_empty() - && let n_consumed = - self.inner_template(&options, f, template, escape, args)? - && n_consumed > 0 - { - args = &args[n_consumed..]; - template = b; - escape = '^'; + let mut args = args.iter(); + let n = first.expand(&options, f, &mut args)?; + while args.len() > n { + mid.expand(&options, f, &mut args)?; + } + if args.len() > 0 { + last.expand(&options, f, &mut args)?; } } } @@ -1050,50 +1115,6 @@ impl TemplateValue { } Ok(()) } - - fn inner_template<'a>( - &self, - options: &ValueOptions, - f: &mut std::fmt::Formatter<'_>, - template: &str, - escape: char, - args: &[Value], - ) -> Result { - let mut iter = template.chars(); - let mut args_consumed = 0; - while let Some(c) = iter.next() { - match c { - '\\' => { - let c = iter.next().unwrap_or('\\') as char; - let c = if c == 'n' { '\n' } else { c }; - write!(f, "{c}")?; - } - c if c == escape => { - let (index, rest) = Self::consume_int(iter.as_str()); - iter = rest.chars(); - if let Some(index) = index.checked_sub(1) - && let Some(arg) = args.get(index) - { - args_consumed = args_consumed.max(index + 1); - write!(f, "{}", arg.display(options))?; - } - } - c => write!(f, "{c}")?, - } - } - Ok(args_consumed) - } - - fn consume_int(input: &str) -> (usize, &str) { - let mut n = 0; - for (index, c) in input.char_indices() { - match c.to_digit(10) { - Some(digit) => n = n * 10 + digit as usize, - None => return (n, &input[index..]), - } - } - (n, "") - } } /// Possible content for a [Value]. diff --git a/rust/pspp/src/spv/read/light.rs b/rust/pspp/src/spv/read/light.rs index 46413b3027..b3b9f4526b 100644 --- a/rust/pspp/src/spv/read/light.rs +++ b/rust/pspp/src/spv/read/light.rs @@ -162,7 +162,6 @@ impl LightTable { } pub fn decode(&self, mut warn: &mut dyn FnMut(LightWarning)) -> PivotTable { - dbg!(self); let encoding = self.formats.encoding(warn); let n1 = self.formats.n1(); diff --git a/rust/pspp/src/spv/read/tests.rs b/rust/pspp/src/spv/read/tests.rs index ac27ae0d3c..9ab4e7b6ca 100644 --- a/rust/pspp/src/spv/read/tests.rs +++ b/rust/pspp/src/spv/read/tests.rs @@ -38,6 +38,12 @@ fn light4() { test_raw_spvfile("light4", None); } +/// Test `[A:B:C]` templates. +#[test] +fn light5() { + test_raw_spvfile("light5", None); +} + #[test] fn legacy1() { test_raw_spvfile("legacy1", None); -- 2.30.2