work
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 9 Jan 2026 17:28:23 +0000 (09:28 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 9 Jan 2026 17:28:23 +0000 (09:28 -0800)
rust/doc/src/spv/light-detail.md
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/value.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/spv/read/tests.rs

index 554a9b83ef74556b71b316e5766178d005cafe25..2eadec547c6465b33e9d9f396a1f16be0aad8905 100644 (file)
@@ -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 `<a> and <b>` for two arguments, `<a>, <b> and <c>`
+      for three arguments, `<a>, <b>, <c>, and <d>` 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
index 933ab3473f7e55fd8dede877a0074bb55c00a246..70f63afd19edaeba8eaa94d5a4a486dffb6cbc3b 100644 (file)
@@ -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,
index fe88f63c50c4c577a050025be494ab70862249e1..263167b61a3eac6d2266c3206f10fbb796eb77ad 100644 (file)
@@ -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<Value>,
+            ) -> Result<usize, std::fmt::Error> {
+                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<usize, std::fmt::Error> {
-        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].
index 46413b302793a67e5eaa61cd4bbb5e203627757a..b3b9f4526b606851a5b820aa9c95dff161bb2ca4 100644 (file)
@@ -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();
index ac27ae0d3cd9bf9c6a3842f8c5e752b998de47b2..9ab4e7b6ca0f5540d657afabfa494706d1baad65 100644 (file)
@@ -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);