work rust
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 7 Jan 2026 22:08:38 +0000 (14:08 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 7 Jan 2026 22:08:38 +0000 (14:08 -0800)
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/value.rs
rust/pspp/src/spv/read/legacy_xml.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/spv/write.rs

index 6a033c451d3d7dbdc129e6272bf8fa385b63a1f2..a2cd1401a770050ca21775db59344724e0b12e37 100644 (file)
@@ -65,7 +65,7 @@ use crate::{
     format::{Decimal, F40, F40_2, F40_3, Format, PCT40_1, Settings as FormatSettings},
     output::pivot::{
         look::{Look, Sizing},
-        value::{BareValue, Value, ValueOptions},
+        value::{BareValue, Value, ValueFormat, ValueOptions},
     },
     settings::{Settings, Show},
     variable::Variable,
@@ -362,9 +362,10 @@ impl PivotTable {
             Class::Residual => F40_2,
             Class::Count => F40, // XXX
         };
-        let value = Value::new_number(number)
-            .with_format(format)
-            .with_honor_small(class == Class::Other);
+        let value = Value::new_number(number).with_format(match class {
+            Class::Other => ValueFormat::SmallE(format),
+            _ => ValueFormat::Other(format),
+        });
         self.insert(data_indexes, value);
     }
 
@@ -1532,9 +1533,7 @@ pub struct PivotTableStyle {
     /// Numeric grouping character (usually `.` or `,`).
     pub grouping: Option<char>,
 
-    /// The threshold for [DatumValue::honor_small].
-    ///
-    /// [DatumValue::honor_small]: value::DatumValue::honor_small
+    /// The threshold for [ValueFormat::SmallE].
     pub small: f64,
 
     /// The format to use for weight and count variables.
index 879e0a6e9939930ebe5c6d84c3526f7bbf9a259b..1f9c21bdce4bad32ef6120d5d0a404b047c796ec 100644 (file)
@@ -125,9 +125,7 @@ impl Value {
         Self::new(ValueInner::Datum(DatumValue::new_number(number)))
     }
 
-    /// Construct a new `Value` from `number` with format [F8_0].
-    ///
-    /// [F8_0]: crate::format::F8_0
+    /// Construct a new `Value` from `number` with format [F40].
     pub fn new_integer(x: Option<f64>) -> Self {
         Self::new_number(x).with_format(F40)
     }
@@ -240,22 +238,13 @@ impl Value {
 
     /// Returns this value with its display format set to `format`, if it is a
     /// [DatumValue].
-    pub fn with_format(self, format: Format) -> Self {
+    pub fn with_format(self, format: impl Into<ValueFormat>) -> Self {
         Self {
             inner: self.inner.with_format(format),
             ..self
         }
     }
 
-    /// Returns this value with `honor_small` set as specified, if it is a
-    /// [DatumValue].
-    pub fn with_honor_small(self, honor_small: bool) -> Self {
-        Self {
-            inner: self.inner.with_honor_small(honor_small),
-            ..self
-        }
-    }
-
     /// Construct a new `Value` from `datum`, which is a value of `variable`.
     pub fn new_datum_from_variable(datum: &Datum<ByteString>, variable: &Variable) -> Self {
         Self::new_datum(&datum.as_encoded(variable.encoding())).with_source_variable(variable)
@@ -652,6 +641,75 @@ impl Debug for Value {
     }
 }
 
+/// A [Format] inside a [Value].
+///
+/// Most `Value`s contain ordinary [Format]s, but occasionally one will have a
+/// special format that is like [Type::F] except that nonzero numbers with
+/// magnitude below a small threshold are instead shown in scientific notation.
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum ValueFormat {
+    /// Any ordinary format.
+    Other(Format),
+
+    /// Displays numbers smaller than [PivotTableStyle::small] in scientific
+    /// notation, and otherwise in the enclosed format (which should be
+    /// [Type::F] format).
+    ///
+    /// [PivotTableStyle::small]: super::PivotTableStyle::small
+    SmallE(Format),
+}
+
+impl ValueFormat {
+    /// Returns the inner [Format].
+    pub fn inner(&self) -> Format {
+        match self {
+            ValueFormat::Other(format) => *format,
+            ValueFormat::SmallE(format) => *format,
+        }
+    }
+
+    /// Returns this format as applied to the given `number` with `small` as the
+    /// threshold.
+    pub fn apply(&self, number: Option<f64>, small: f64) -> Format {
+        if let ValueFormat::SmallE(format) = self
+            && let Some(number) = number
+            && number != 0.0
+            && number.abs() < small
+        {
+            UncheckedFormat::new(Type::E, 40, format.d() as u8).fix()
+        } else {
+            self.inner()
+        }
+    }
+
+    /// Returns true if this is [ValueFormat::SmallE].
+    pub fn is_small_e(&self) -> bool {
+        matches!(self, ValueFormat::SmallE(_))
+    }
+}
+
+impl From<Format> for ValueFormat {
+    fn from(format: Format) -> Self {
+        Self::Other(format)
+    }
+}
+
+impl Serialize for ValueFormat {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        match self {
+            ValueFormat::Other(format) => format.serialize(serializer),
+            ValueFormat::SmallE(format) => {
+                #[derive(Serialize)]
+                struct SmallE(Format);
+                SmallE(*format).serialize(serializer)
+            }
+        }
+    }
+}
+
 /// A datum and how to display it.
 #[derive(Clone, Debug, PartialEq)]
 pub struct DatumValue {
@@ -659,20 +717,13 @@ pub struct DatumValue {
     pub datum: Datum<WithEncoding<ByteString>>,
 
     /// The display format.
-    pub format: Format,
+    pub format: ValueFormat,
 
     /// Whether to show `value` or `value_label` or both.
     ///
     /// If this is unset, then a higher-level default is used.
     pub show: Option<Show>,
 
-    /// If true, then numbers smaller than [PivotTableStyle::small] will be
-    /// displayed in scientific notation.  Otherwise, all numbers will be
-    /// displayed with `format`.
-    ///
-    /// [PivotTableStyle::small]: super::PivotTableStyle::small
-    pub honor_small: bool,
-
     /// The name of the variable that `value` came from, if any.
     pub variable: Option<String>,
 
@@ -685,7 +736,11 @@ impl Serialize for DatumValue {
     where
         S: serde::Serializer,
     {
-        if self.format.type_() == Type::F && self.variable.is_none() && self.value_label.is_none() {
+        if let ValueFormat::Other(format) = self.format
+            && format.type_() == Type::F
+            && self.variable.is_none()
+            && self.value_label.is_none()
+        {
             self.datum.serialize(serializer)
         } else {
             let mut s = serializer.serialize_map(None)?;
@@ -694,9 +749,6 @@ impl Serialize for DatumValue {
             if let Some(show) = self.show {
                 s.serialize_entry("show", &show)?;
             }
-            if self.honor_small {
-                s.serialize_entry("honor_small", &self.honor_small)?;
-            }
             if let Some(variable) = &self.variable {
                 s.serialize_entry("variable", variable)?;
             }
@@ -716,9 +768,8 @@ impl DatumValue {
     {
         Self {
             datum: datum.cloned(),
-            format: F8_2,
+            format: ValueFormat::Other(F8_2),
             show: None,
-            honor_small: false,
             variable: None,
             value_label: None,
         }
@@ -730,18 +781,10 @@ impl DatumValue {
     }
 
     /// Returns this `DatumValue` with the given `format`.
-    pub fn with_format(self, format: Format) -> Self {
+    pub fn with_format(self, format: ValueFormat) -> Self {
         Self { format, ..self }
     }
 
-    /// Returns this `DatumValue` with the given `honor_small`.
-    pub fn with_honor_small(self, honor_small: bool) -> Self {
-        Self {
-            honor_small,
-            ..self
-        }
-    }
-
     /// Writes this value to `f` using the settings in `display`.
     pub fn display<'a>(
         &self,
@@ -751,16 +794,7 @@ impl DatumValue {
         if display.show_value {
             match &self.datum {
                 Datum::Number(number) => {
-                    let format = if self.format.type_() == Type::F
-                        && self.honor_small
-                        && let Some(number) = *number
-                        && number != 0.0
-                        && number.abs() < display.small()
-                    {
-                        UncheckedFormat::new(Type::E, 40, self.format.d() as u8).fix()
-                    } else {
-                        self.format
-                    };
+                    let format = self.format.apply(*number, display.small());
                     self.datum
                         .display(format)
                         .with_settings(&display.options.settings)
@@ -768,7 +802,7 @@ impl DatumValue {
                         .fmt(f)?;
                 }
                 Datum::String(s) => {
-                    if self.format.type_() == Type::AHex {
+                    if self.format.inner().type_() == Type::AHex {
                         write!(f, "{}", s.inner.display_hex())?;
                     } else {
                         f.write_str(&s.as_str())?;
@@ -787,7 +821,7 @@ impl DatumValue {
 
     /// Returns the decimal point used in the formatted value, if any.
     pub fn decimal(&self) -> Decimal {
-        self.datum.display(self.format).decimal()
+        self.datum.display(self.format.inner()).decimal()
     }
 
     /// Serializes this value to `serializer` in the "bare" manner described for
@@ -1091,18 +1125,9 @@ impl ValueInner {
 
     /// Returns this value with its display format set to `format`, if it is a
     /// [DatumValue].
-    pub fn with_format(mut self, format: Format) -> Self {
-        if let Some(datum_value) = self.as_datum_value_mut() {
-            datum_value.format = format;
-        }
-        self
-    }
-
-    /// Returns this value with `honor_small` set as specified, if it is a
-    /// [DatumValue].
-    pub fn with_honor_small(mut self, honor_small: bool) -> Self {
+    pub fn with_format(mut self, format: impl Into<ValueFormat>) -> Self {
         if let Some(datum_value) = self.as_datum_value_mut() {
-            datum_value.honor_small = honor_small;
+            datum_value.format = format.into();
         }
         self
     }
index 47c2ca09deb8df714bed4e5a278f05a153b89531..b0e887475fa013bf9e01eaa4b9a1c454da613fcb 100644 (file)
@@ -39,7 +39,7 @@ use crate::{
     output::pivot::{
         self, Axis2, Axis3, Category, CategoryLocator, Dimension, Group, Leaf, Length, PivotTable,
         look::{self, Area, AreaStyle, CellStyle, Color, HorzAlign, Look, RowParity, VertAlign},
-        value::Value,
+        value::{Value, ValueFormat},
     },
     spv::read::light::decode_format,
 };
@@ -54,7 +54,7 @@ pub fn datum_as_format(datum: &Datum<String>) -> crate::format::Format {
         Datum::Number(None) => 0,
         Datum::String(s) => s.parse().unwrap_or_default(),
     };
-    decode_format(f as u32, &mut |_| () /*XXX*/).0
+    decode_format(f as u32, &mut |_| () /*XXX*/).inner()
 }
 
 /// Returns this data value interpreted using `format`.
@@ -1143,7 +1143,7 @@ impl Style {
             {
                 match &datum_value.datum {
                     Datum::Number(_) => {
-                        datum_value.format = format;
+                        datum_value.format = ValueFormat::Other(format);
                     }
                     Datum::String(string) => {
                         if format.type_().category() == format::Category::Date
@@ -1157,7 +1157,9 @@ impl Style {
                             && let Ok(time) =
                                 NaiveTime::parse_from_str(&string.as_str(), "%H:%M:%S%.3f")
                         {
-                            value.inner = Value::new_time(time).with_format(format).inner;
+                            value.inner = Value::new_time(time)
+                                .with_format(ValueFormat::Other(format))
+                                .inner;
                         } else if let Ok(number) = string.as_str().parse::<f64>() {
                             value.inner = Value::new_number(Some(number)).with_format(format).inner;
                         }
index 1b1aca730cc775fc62b252c63845c54d207d29e7..b2d7638b3bb30ad3439919971023bf72425a46b2 100644 (file)
@@ -19,8 +19,8 @@ use enum_map::{EnumMap, enum_map};
 use crate::{
     data::Datum,
     format::{
-        self, CC, Decimal, Decimals, Epoch, F40, F40_2, NumberStyle, Settings, Type,
-        UncheckedFormat, Width,
+        CC, Decimal, Decimals, Epoch, F40, F40_2, NumberStyle, Settings, Type, UncheckedFormat,
+        Width,
     },
     output::pivot::{
         self, Axis, Axis2, Axis3, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group,
@@ -30,7 +30,7 @@ use crate::{
             RowColBorder, RowParity, Stroke, VertAlign,
         },
         parse_bool,
-        value::{self, DatumValue, TemplateValue, ValueStyle, VariableValue},
+        value::{self, DatumValue, TemplateValue, ValueFormat, ValueStyle, VariableValue},
     },
     settings,
 };
@@ -162,6 +162,7 @@ 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();
@@ -178,10 +179,11 @@ impl LightTable {
             .cells
             .iter()
             .map(|cell| {
-                (
-                    PrecomputedIndex(cell.index as usize),
-                    cell.value.decode(encoding, &footnotes, warn),
-                )
+                (PrecomputedIndex(cell.index as usize), {
+                    let value = cell.value.decode(encoding, &footnotes, warn);
+                    dbg!(value.inner.as_datum_value());
+                    value
+                })
             })
             .collect::<Vec<_>>();
         let dimensions = self
@@ -1143,7 +1145,7 @@ struct ValueNumber {
 struct Format(u32);
 
 impl Format {
-    pub fn decode(&self, warn: &mut dyn FnMut(LightWarning)) -> (format::Format, bool) {
+    pub fn decode(&self, warn: &mut dyn FnMut(LightWarning)) -> ValueFormat {
         decode_format(self.0, warn)
     }
 }
@@ -1151,13 +1153,16 @@ impl Format {
 impl Debug for Format {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let mut warning = false;
-        let (format, _honor_small) = self.decode(&mut |_| {
+        let format = self.decode(&mut |_| {
             warning = true;
         });
         if warning {
             write!(f, "InvalidFormat({:#x})", self.0)
         } else {
-            write!(f, "{format}")
+            match format {
+                ValueFormat::Other(format) => write!(f, "{format}"),
+                ValueFormat::SmallE(format) => write!(f, "SmallE({format})"),
+            }
         }
     }
 }
@@ -1287,27 +1292,30 @@ impl BinRead for Value {
     }
 }
 
-pub(super) fn decode_format(
-    raw: u32,
-    warn: &mut dyn FnMut(LightWarning),
-) -> (format::Format, bool) {
+pub(super) fn decode_format(raw: u32, warn: &mut dyn FnMut(LightWarning)) -> ValueFormat {
     if raw == 0 || raw == 0x10000 || raw == 1 {
-        return (F40_2, false);
+        return ValueFormat::Other(F40_2);
     }
 
     let raw_type = (raw >> 16) as u16;
-    let (type_, honor_small) = if raw_type >= 40 {
-        (Type::F, true)
+    let type_ = if raw_type >= 40 {
+        Type::F
     } else if let Ok(type_) = Type::try_from(raw_type) {
-        (type_, false)
+        type_
     } else {
         warn(LightWarning::InvalidFormat(raw_type));
-        (Type::F, false)
+        Type::F
     };
+
     let w = ((raw >> 8) & 0xff) as Width;
     let d = raw as Decimals;
+    let inner = UncheckedFormat::new(type_, w, d).fix();
 
-    (UncheckedFormat::new(type_, w, d).fix(), honor_small)
+    if raw_type >= 40 {
+        ValueFormat::SmallE(inner)
+    } else {
+        ValueFormat::Other(inner)
+    }
 }
 
 impl ValueNumber {
@@ -1317,10 +1325,9 @@ impl ValueNumber {
         footnotes: &pivot::Footnotes,
         warn: &mut dyn FnMut(LightWarning),
     ) -> value::Value {
-        let (format, honor_small) = dbg!(self.format.decode(warn));
+        let format = dbg!(self.format.decode(warn));
         value::Value::new_number((self.x != -f64::MAX).then_some(self.x))
             .with_format(format)
-            .with_honor_small(honor_small)
             .with_styling(ValueMods::decode_optional(&self.mods, encoding, footnotes))
     }
 }
@@ -1332,10 +1339,9 @@ impl ValueVarNumber {
         footnotes: &pivot::Footnotes,
         warn: &mut dyn FnMut(LightWarning),
     ) -> value::Value {
-        let (format, honor_small) = self.format.decode(warn);
+        let format = self.format.decode(warn);
         value::Value::new_number((self.x != -f64::MAX).then_some(self.x))
             .with_format(format)
-            .with_honor_small(honor_small)
             .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)))
@@ -1362,11 +1368,10 @@ impl ValueString {
         footnotes: &pivot::Footnotes,
         warn: &mut dyn FnMut(LightWarning),
     ) -> value::Value {
-        let (format, honor_small) = self.format.decode(warn);
+        let format = self.format.decode(warn);
         value::Value::new(pivot::value::ValueInner::Datum(DatumValue {
             datum: Datum::new_utf8(self.s.decode(encoding)),
             format,
-            honor_small,
             show: self.show.decode(warn),
             variable: self.var_name.decode_optional(encoding),
             value_label: self.value_label.decode_optional(encoding),
index 1276a29a7ceeb0c5a3f7d71f7f8a20ab5d1ef0e5..4e73d09133bd72916eb5ee23ea5c0ad55575999f 100644 (file)
@@ -43,7 +43,7 @@ use crate::{
                 HeadingRegion, HorzAlign, LabelPosition, RowColBorder, RowParity, Stroke,
                 VertAlign,
             },
-            value::{Value, ValueInner, ValueStyle},
+            value::{Value, ValueFormat, ValueInner, ValueStyle},
         },
     },
     settings::Show,
@@ -1221,12 +1221,7 @@ impl<'a> BinWrite for ValueMod<'a> {
     }
 }
 
-struct SpvFormat {
-    format: Format,
-    honor_small: bool,
-}
-
-impl BinWrite for SpvFormat {
+impl BinWrite for ValueFormat {
     type Args<'a> = ();
 
     fn write_options<W: Write + Seek>(
@@ -1235,12 +1230,15 @@ impl BinWrite for SpvFormat {
         endian: binrw::Endian,
         args: Self::Args<'_>,
     ) -> binrw::BinResult<()> {
-        let type_ = if self.format.type_() == Type::F && self.honor_small {
+        let type_ = if let ValueFormat::SmallE(format) = self
+            && format.type_() == Type::F
+        {
             40
         } else {
-            self.format.type_().into()
+            self.inner().type_().into()
         };
-        (((type_ as u32) << 16) | ((self.format.w() as u32) << 8) | (self.format.d() as u32))
+        let inner = self.inner();
+        (((type_ as u32) << 16) | ((inner.w() as u32) << 8) | (inner.d() as u32))
             .write_options(writer, endian, args)
     }
 }
@@ -1257,15 +1255,11 @@ impl BinWrite for Value {
         match &self.inner {
             ValueInner::Datum(number_value) => match &number_value.datum {
                 Datum::Number(number) => {
-                    let format = SpvFormat {
-                        format: number_value.format,
-                        honor_small: number_value.honor_small,
-                    };
                     if number_value.variable.is_some() || number_value.value_label.is_some() {
                         (
                             2u8,
                             ValueMod::new(self),
-                            format,
+                            number_value.format,
                             number.unwrap_or(f64::MIN),
                             SpvString::optional(&number_value.variable),
                             SpvString::optional(&number_value.value_label),
@@ -1273,14 +1267,19 @@ impl BinWrite for Value {
                         )
                             .write_options(writer, endian, args)?;
                     } else {
-                        (1u8, ValueMod::new(self), format, number.unwrap_or(f64::MIN))
+                        (
+                            1u8,
+                            ValueMod::new(self),
+                            number_value.format,
+                            number.unwrap_or(f64::MIN),
+                        )
                             .write_options(writer, endian, args)?;
                     }
                 }
                 Datum::String(s) => {
                     let hex;
                     let utf8;
-                    let (s, format) = if number_value.format.type_() == Type::AHex {
+                    let (s, format) = if number_value.format.inner().type_() == Type::AHex {
                         hex = s.inner.to_hex();
                         (
                             hex.as_str(),
@@ -1296,10 +1295,7 @@ impl BinWrite for Value {
                     (
                         4u8,
                         ValueMod::new(self),
-                        SpvFormat {
-                            format,
-                            honor_small: false,
-                        },
+                        ValueFormat::Other(format),
                         SpvString::optional(&number_value.value_label),
                         SpvString::optional(&number_value.variable),
                         Show::as_spv(&number_value.show),