work
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 8 Jan 2026 22:30:24 +0000 (14:30 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 8 Jan 2026 22:30:24 +0000 (14:30 -0800)
rust/pspp/src/format.rs
rust/pspp/src/spv/read/light.rs

index c45da56aae2d795d17d42a92df123ed6217f550f..f180434b73970967e775e095a75c91ca8475a9ee 100644 (file)
@@ -1013,6 +1013,10 @@ pub struct Settings {
     /// instead of `.5`)?
     pub leading_zero: bool,
 
+    /// Format `PCT` and `DOLLAR` with leading zero (e.g. `$0.5` instead of
+    /// `$.5`)?
+    pub leading_zero_pct: bool,
+
     /// Custom currency styles.
     pub ccs: EnumMap<CC, Option<Box<NumberStyle>>>,
 }
@@ -1021,12 +1025,14 @@ pub struct Settings {
 struct StyleParams {
     decimal: Decimal,
     leading_zero: bool,
+    leading_zero_pct: bool,
 }
 impl From<&Settings> for StyleParams {
     fn from(value: &Settings) -> Self {
         Self {
             decimal: value.decimal,
             leading_zero: value.leading_zero,
+            leading_zero_pct: value.leading_zero_pct,
         }
     }
 }
@@ -1042,6 +1048,48 @@ impl StyleSet {
     }
 }
 
+struct NumberStyles {
+    f: StyleSet,
+    comma: StyleSet,
+    dot: StyleSet,
+    dollar: StyleSet,
+    pct: StyleSet,
+    default: NumberStyle,
+}
+impl NumberStyles {
+    fn new() -> Self {
+        Self {
+            f: StyleSet::new(|p| NumberStyle::new(p.decimal, p.leading_zero)),
+            comma: StyleSet::new(|p| {
+                NumberStyle::new(p.decimal, p.leading_zero).with_grouping(true)
+            }),
+            dot: StyleSet::new(|p| {
+                NumberStyle::new(!p.decimal, p.leading_zero).with_grouping(true)
+            }),
+            dollar: StyleSet::new(|p| {
+                NumberStyle::new(p.decimal, p.leading_zero_pct)
+                    .with_grouping(true)
+                    .with_prefix("$")
+            }),
+            pct: StyleSet::new(|p| {
+                NumberStyle::new(p.decimal, p.leading_zero_pct).with_suffix("%")
+            }),
+            default: NumberStyle::new(Decimal::Dot, false),
+        }
+    }
+    fn get<'a>(&'a self, settings: &'a Settings, type_: Type) -> &'a NumberStyle {
+        match type_ {
+            Type::F | Type::E => self.f.get(settings),
+            Type::Comma => self.comma.get(settings),
+            Type::Dot => self.dot.get(settings),
+            Type::Dollar => self.dollar.get(settings),
+            Type::Pct => self.pct.get(settings),
+            Type::CC(cc) => settings.ccs[cc].as_deref().unwrap_or(&self.default),
+            _ => &self.default,
+        }
+    }
+}
+
 impl Settings {
     pub fn with_cc(mut self, cc: CC, style: NumberStyle) -> Self {
         self.ccs[cc] = Some(Box::new(style));
@@ -1053,76 +1101,18 @@ impl Settings {
             ..self
         }
     }
+    pub fn with_leading_zero_pct(self, leading_zero_pct: bool) -> Self {
+        Self {
+            leading_zero_pct,
+            ..self
+        }
+    }
     pub fn with_epoch(self, epoch: Epoch) -> Self {
         Self { epoch, ..self }
     }
     pub fn number_style(&self, type_: Type) -> &NumberStyle {
-        static DEFAULT: LazyLock<NumberStyle> =
-            LazyLock::new(|| NumberStyle::new("", "", Decimal::Dot, None, false));
-
-        match type_ {
-            Type::F | Type::E => {
-                static F: LazyLock<StyleSet> = LazyLock::new(|| {
-                    StyleSet::new(|p| NumberStyle::new("", "", p.decimal, None, p.leading_zero))
-                });
-                F.get(self)
-            }
-            Type::Comma => {
-                static COMMA: LazyLock<StyleSet> = LazyLock::new(|| {
-                    StyleSet::new(|p| {
-                        NumberStyle::new("", "", p.decimal, Some(!p.decimal), p.leading_zero)
-                    })
-                });
-                COMMA.get(self)
-            }
-            Type::Dot => {
-                static DOT: LazyLock<StyleSet> = LazyLock::new(|| {
-                    StyleSet::new(|p| {
-                        NumberStyle::new("", "", !p.decimal, Some(p.decimal), p.leading_zero)
-                    })
-                });
-                DOT.get(self)
-            }
-            Type::Dollar => {
-                static DOLLAR: LazyLock<StyleSet> = LazyLock::new(|| {
-                    StyleSet::new(|p| NumberStyle::new("$", "", p.decimal, Some(!p.decimal), false))
-                });
-                DOLLAR.get(self)
-            }
-            Type::Pct => {
-                static PCT: LazyLock<StyleSet> = LazyLock::new(|| {
-                    StyleSet::new(|p| NumberStyle::new("", "%", p.decimal, None, false))
-                });
-                PCT.get(self)
-            }
-            Type::CC(cc) => self.ccs[cc].as_deref().unwrap_or(&DEFAULT),
-            Type::N
-            | Type::Z
-            | Type::P
-            | Type::PK
-            | Type::IB
-            | Type::PIB
-            | Type::PIBHex
-            | Type::RB
-            | Type::RBHex
-            | Type::Date
-            | Type::ADate
-            | Type::EDate
-            | Type::JDate
-            | Type::SDate
-            | Type::QYr
-            | Type::MoYr
-            | Type::WkYr
-            | Type::DateTime
-            | Type::YmdHms
-            | Type::MTime
-            | Type::Time
-            | Type::DTime
-            | Type::WkDay
-            | Type::Month
-            | Type::A
-            | Type::AHex => &DEFAULT,
-        }
+        static NUMBER_STYLES: LazyLock<NumberStyles> = LazyLock::new(|| NumberStyles::new());
+        NUMBER_STYLES.get(self, type_)
     }
 }
 
@@ -1178,29 +1168,64 @@ impl Display for NumberStyle {
 }
 
 impl NumberStyle {
-    fn new(
-        prefix: &str,
-        suffix: &str,
-        decimal: Decimal,
-        grouping: Option<Decimal>,
-        leading_zero: bool,
-    ) -> Self {
-        // These assertions ensure that zero is correct for `extra_bytes`.
-        debug_assert!(prefix.is_ascii());
-        debug_assert!(suffix.is_ascii());
-
+    fn new(decimal: Decimal, leading_zero: bool) -> Self {
         Self {
-            neg_prefix: Affix::new("-"),
-            prefix: Affix::new(prefix),
-            suffix: Affix::new(suffix),
-            neg_suffix: Affix::new(""),
+            neg_prefix: Affix::from("-"),
+            prefix: Affix::new(),
+            suffix: Affix::new(),
+            neg_suffix: Affix::new(),
             decimal,
-            grouping,
+            grouping: None,
             leading_zero,
             extra_bytes: 0,
         }
     }
 
+    fn with_grouping(self, grouping: bool) -> Self {
+        Self {
+            grouping: grouping.then_some(!self.decimal),
+            ..self
+        }
+    }
+
+    fn with_prefix(self, prefix: impl Into<String>) -> Self {
+        let prefix = Affix::from(prefix);
+        Self {
+            extra_bytes: self.extra_bytes - self.prefix.extra_bytes() + prefix.extra_bytes(),
+            prefix,
+            ..self
+        }
+    }
+
+    fn with_neg_prefix(self, neg_prefix: impl Into<String>) -> Self {
+        let neg_prefix = Affix::from(neg_prefix);
+        Self {
+            extra_bytes: self.extra_bytes - self.neg_prefix.extra_bytes()
+                + neg_prefix.extra_bytes(),
+            neg_prefix,
+            ..self
+        }
+    }
+
+    fn with_neg_suffix(self, neg_suffix: impl Into<String>) -> Self {
+        let neg_suffix = Affix::from(neg_suffix);
+        Self {
+            extra_bytes: self.extra_bytes - self.neg_suffix.extra_bytes()
+                + neg_suffix.extra_bytes(),
+            neg_suffix,
+            ..self
+        }
+    }
+
+    fn with_suffix(self, suffix: impl Into<String>) -> Self {
+        let suffix = Affix::from(suffix);
+        Self {
+            extra_bytes: self.extra_bytes - self.suffix.extra_bytes() + suffix.extra_bytes(),
+            suffix,
+            ..self
+        }
+    }
+
     fn affix_width(&self) -> usize {
         self.prefix.width + self.suffix.width
     }
@@ -1216,14 +1241,23 @@ pub struct Affix {
     pub width: usize,
 }
 
-impl Affix {
-    fn new(s: impl Into<String>) -> Self {
-        let s = s.into();
+impl<T> From<T> for Affix
+where
+    T: Into<String>,
+{
+    fn from(value: T) -> Self {
+        let s = value.into();
         Self {
             width: s.width(),
             s,
         }
     }
+}
+
+impl Affix {
+    fn new() -> Self {
+        Self::from("")
+    }
 
     fn extra_bytes(&self) -> usize {
         self.s.len().checked_sub(self.width).unwrap()
@@ -1283,7 +1317,7 @@ impl FromStr for NumberStyle {
             }
         }
 
-        fn take_cc_token(iter: &mut Chars<'_>, grouping: char) -> Affix {
+        fn take_cc_token(iter: &mut Chars<'_>, grouping: char) -> String {
             let mut s = String::new();
             let mut quote = false;
             for c in iter {
@@ -1296,7 +1330,7 @@ impl FromStr for NumberStyle {
                     quote = false;
                 }
             }
-            Affix::new(s)
+            s
         }
 
         let Some(grouping) = find_separator(s) else {
@@ -1309,20 +1343,12 @@ impl FromStr for NumberStyle {
         let neg_suffix = take_cc_token(&mut iter, grouping);
         let grouping: Decimal = grouping.try_into().unwrap();
         let decimal = !grouping;
-        let extra_bytes = neg_prefix.extra_bytes()
-            + prefix.extra_bytes()
-            + suffix.extra_bytes()
-            + neg_suffix.extra_bytes();
-        Ok(Self {
-            neg_prefix,
-            prefix,
-            suffix,
-            neg_suffix,
-            decimal,
-            grouping: Some(grouping),
-            leading_zero: false,
-            extra_bytes,
-        })
+        Ok(Self::new(decimal, false)
+            .with_grouping(true)
+            .with_prefix(prefix)
+            .with_neg_prefix(neg_prefix)
+            .with_neg_suffix(neg_suffix)
+            .with_suffix(suffix))
     }
 }
 
index b2d7638b3bb30ad3439919971023bf72425a46b2..0c627fed0b4ef157ffd83d90aa3e8f307cdd07b1 100644 (file)
@@ -179,11 +179,10 @@ impl LightTable {
             .cells
             .iter()
             .map(|cell| {
-                (PrecomputedIndex(cell.index as usize), {
-                    let value = cell.value.decode(encoding, &footnotes, warn);
-                    dbg!(value.inner.as_datum_value());
-                    value
-                })
+                (
+                    PrecomputedIndex(cell.index as usize),
+                    cell.value.decode(encoding, &footnotes, warn),
+                )
             })
             .collect::<Vec<_>>();
         let dimensions = self
@@ -231,7 +230,12 @@ impl LightTable {
                 settings: Arc::new(Settings {
                     epoch: self.formats.y0.epoch(),
                     decimal: self.formats.y0.decimal(warn),
-                    leading_zero: y1.map_or(false, |y1| y1.include_leading_zero),
+                    leading_zero: if let Some(y1) = y1 {
+                        y1.include_leading_zero
+                    } else {
+                        false
+                    },
+                    leading_zero_pct: true,
                     ccs: self.formats.custom_currency.decode(encoding, warn),
                 }),
                 grouping: {
@@ -865,10 +869,13 @@ struct Formats {
 
 impl Formats {
     fn y1(&self) -> Option<&Y1> {
-        self.v1
-            .as_ref()
-            .map(|n0| &n0.y1)
-            .or_else(|| self.v3.as_ref().map(|v3| &v3.n3.y1))
+        if let Some(n0) = &**self.v1 {
+            Some(&n0.y1)
+        } else if let Some(v3) = &self.v3 {
+            Some(&v3.n3.y1)
+        } else {
+            None
+        }
     }
 
     fn n1(&self) -> Option<&N1> {
@@ -1325,9 +1332,8 @@ impl ValueNumber {
         footnotes: &pivot::Footnotes,
         warn: &mut dyn FnMut(LightWarning),
     ) -> value::Value {
-        let format = dbg!(self.format.decode(warn));
         value::Value::new_number((self.x != -f64::MAX).then_some(self.x))
-            .with_format(format)
+            .with_format(self.format.decode(warn))
             .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
     }
 }
@@ -1339,9 +1345,8 @@ impl ValueVarNumber {
         footnotes: &pivot::Footnotes,
         warn: &mut dyn FnMut(LightWarning),
     ) -> value::Value {
-        let format = self.format.decode(warn);
         value::Value::new_number((self.x != -f64::MAX).then_some(self.x))
-            .with_format(format)
+            .with_format(self.format.decode(warn))
             .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
             .with_value_label(self.value_label.decode_optional(encoding))
             .with_variable_name(Some(self.var_name.decode(encoding)))
@@ -1368,10 +1373,9 @@ impl ValueString {
         footnotes: &pivot::Footnotes,
         warn: &mut dyn FnMut(LightWarning),
     ) -> value::Value {
-        let format = self.format.decode(warn);
         value::Value::new(pivot::value::ValueInner::Datum(DatumValue {
             datum: Datum::new_utf8(self.s.decode(encoding)),
-            format,
+            format: self.format.decode(warn),
             show: self.show.decode(warn),
             variable: self.var_name.decode_optional(encoding),
             value_label: self.value_label.decode_optional(encoding),