spv progress
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 15 May 2025 00:07:32 +0000 (17:07 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 15 May 2025 00:07:32 +0000 (17:07 -0700)
rust/pspp/src/format/mod.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/spv.rs

index e03fd714e97ede4d55cb8f6359dcbcaa19124b3d..7094dec8a4a7cb3310421dc1d574292a98e09321 100644 (file)
@@ -1,5 +1,5 @@
 use std::{
-    fmt::{Debug, Display, Formatter, Result as FmtResult},
+    fmt::{Debug, Display, Formatter, Result as FmtResult, Write},
     ops::{Not, RangeInclusive},
     str::{Chars, FromStr},
     sync::LazyLock,
@@ -938,7 +938,7 @@ impl Settings {
     pub fn with_epoch(self, epoch: Epoch) -> Self {
         Self { epoch, ..self }
     }
-    fn number_style(&self, type_: Type) -> &NumberStyle {
+    pub fn number_style(&self, type_: Type) -> &NumberStyle {
         static DEFAULT: LazyLock<NumberStyle> =
             LazyLock::new(|| NumberStyle::new("", "", Decimal::Dot, None, false));
 
@@ -1036,6 +1036,28 @@ pub struct NumberStyle {
     pub extra_bytes: usize,
 }
 
+impl Display for NumberStyle {
+    /// Display this number style in the format used for custom currency.
+    ///
+    /// This format can only accurately represent number styles that include a
+    /// grouping character.  If this number style doesn't, it will pretend that
+    /// the grouping character is the opposite of the decimal point character.
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        let grouping = char::from(!self.decimal);
+        write!(
+            f,
+            "{}{}{}{}{}{}{}",
+            self.neg_prefix.display(grouping),
+            grouping,
+            self.prefix.display(grouping),
+            grouping,
+            self.suffix.display(grouping),
+            grouping,
+            self.neg_suffix.display(grouping),
+        )
+    }
+}
+
 impl NumberStyle {
     fn new(
         prefix: &str,
@@ -1086,6 +1108,30 @@ impl Affix {
     fn extra_bytes(&self) -> usize {
         self.s.len().checked_sub(self.width).unwrap()
     }
+
+    fn display(&self, escape: char) -> DisplayAffix<'_> {
+        DisplayAffix {
+            affix: self.s.as_str(),
+            escape,
+        }
+    }
+}
+
+pub struct DisplayAffix<'a> {
+    affix: &'a str,
+    escape: char,
+}
+
+impl Display for DisplayAffix<'_> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        for c in self.affix.chars() {
+            if c == self.escape {
+                f.write_char('\'')?;
+            }
+            f.write_char(c)?;
+        }
+        Ok(())
+    }
 }
 
 impl FromStr for NumberStyle {
index 1f5ffea12ec8e1b3804031da500290c09ac3fb36..0160885211821248033e7510ea16a6b5b31dd08d 100644 (file)
@@ -336,11 +336,12 @@ pub struct Dimension {
 
     /// Ordering of leaves for presentation.
     ///
-    /// This is a permutation of `0..n` where `n` is the number of leaves.
-    presentation_order: Vec<usize>,
+    /// This is a permutation of `0..n` where `n` is the number of leaves.  It
+    /// maps from an index in presentation order to an index in data order.
+    pub presentation_order: Vec<usize>,
 
     /// Display.
-    hide_all_labels: bool,
+    pub hide_all_labels: bool,
 }
 
 pub type GroupVec<'a> = SmallVec<[&'a Group; 4]>;
@@ -379,13 +380,13 @@ impl Dimension {
 #[derive(Clone, Debug)]
 pub struct Group {
     len: usize,
-    name: Box<Value>,
+    pub name: Box<Value>,
 
     /// The child categories.
     ///
     /// A group usually has multiple children, but it is allowed to have
     /// only one or even (pathologically) none.
-    children: Vec<Category>,
+    pub children: Vec<Category>,
 
     /// Whether to show the group's label.
     pub show_label: bool,
@@ -454,6 +455,10 @@ impl Group {
     pub fn is_empty(&self) -> bool {
         self.len() == 0
     }
+
+    pub fn name(&self) -> &Value {
+        &self.name
+    }
 }
 
 #[derive(Clone, Debug, Default)]
@@ -486,6 +491,9 @@ impl Leaf {
             name: Box::new(name),
         }
     }
+    pub fn name(&self) -> &Value {
+        &self.name
+    }
 }
 
 /// Pivot result classes.
@@ -511,14 +519,21 @@ pub enum Category {
 }
 
 impl Category {
-    fn len(&self) -> usize {
+    pub fn name(&self) -> &Value {
+        match self {
+            Category::Group(group) => &group.name,
+            Category::Leaf(leaf) => &leaf.name,
+        }
+    }
+
+    pub fn len(&self) -> usize {
         match self {
             Category::Group(group) => group.len,
             Category::Leaf(_) => 1,
         }
     }
 
-    fn nth_leaf(&self, index: usize) -> Option<&Leaf> {
+    pub fn nth_leaf(&self, index: usize) -> Option<&Leaf> {
         match self {
             Category::Group(group) => group.nth_leaf(index),
             Category::Leaf(leaf) => {
@@ -531,7 +546,7 @@ impl Category {
         }
     }
 
-    fn leaf_path<'a>(&'a self, index: usize, groups: GroupVec<'a>) -> Option<Path<'a>> {
+    pub fn leaf_path<'a>(&'a self, index: usize, groups: GroupVec<'a>) -> Option<Path<'a>> {
         match self {
             Category::Group(group) => group.leaf_path(index, groups),
             Category::Leaf(leaf) => {
@@ -544,7 +559,7 @@ impl Category {
         }
     }
 
-    fn show_label(&self) -> bool {
+    pub fn show_label(&self) -> bool {
         match self {
             Category::Group(group) => group.show_label,
             Category::Leaf(_) => true,
index eea552b2de6a4ac4b033c9e10e4fd2fd78f23acd..df39a7126c07a884de0422449a888df24b6c4054 100644 (file)
@@ -3,10 +3,12 @@ use std::{
     borrow::Cow,
     fmt::Write as _,
     io::{Cursor, Result as IoResult, Seek, Write},
+    iter::{repeat, repeat_n},
     sync::Arc,
 };
 
 use binrw::{BinWrite, Endian};
+use enum_map::EnumMap;
 use quick_xml::{
     events::{attributes::Attribute, BytesText},
     writer::Writer as XmlWriter,
@@ -21,9 +23,10 @@ use crate::{
     output::{
         driver::Driver,
         pivot::{
-            Area, AreaStyle, Axis2, Axis3, Border, BorderStyle, BoxBorder, CellStyle, Color,
-            FontStyle, Footnote, Footnotes, HeadingRegion, HorzAlign, PivotTable, RowColBorder,
-            Stroke, Value, ValueInner, ValueStyle, VertAlign,
+            Area, AreaStyle, Axis2, Axis3, Border, BorderStyle, BoxBorder, Category, CellStyle,
+            Color, Dimension, FontStyle, Footnote, FootnoteMarkerPosition, FootnoteMarkerType,
+            Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Leaf, PivotTable,
+            RowColBorder, Stroke, Value, ValueInner, ValueStyle, VertAlign,
         },
         Item,
     },
@@ -97,6 +100,27 @@ where
             }),
         }
     }
+
+    fn write_item(&mut self, item: &Item) -> Option<Container> {
+        match &item.details {
+            super::Details::Chart => todo!(),
+            super::Details::Image => todo!(),
+            super::Details::Group(children) => {
+                let containers = children
+                    .iter()
+                    .map(|child| self.write_item(child))
+                    .flatten()
+                    .collect::<Vec<_>>();
+            }
+            super::Details::Message(_diagnostic) => todo!(),
+            super::Details::PageBreak => {
+                self.needs_page_break = true;
+                None
+            }
+            super::Details::Table(pivot_table) => Some(self.write_table(&*item, pivot_table)),
+            super::Details::Text(_text) => todo!(),
+        }
+    }
 }
 
 impl BinWrite for PivotTable {
@@ -105,11 +129,24 @@ impl BinWrite for PivotTable {
     fn write_options<W: Write + Seek>(
         &self,
         writer: &mut W,
-        _endian: Endian,
+        endian: Endian,
         _args: (),
     ) -> binrw::BinResult<()> {
         // Header.
-        Header::new(self).write_le(writer)?;
+        (
+            1u16,
+            SpvBool(true),  // x0
+            SpvBool(false), // x1
+            SpvBool(self.rotate_inner_column_labels),
+            SpvBool(self.rotate_outer_row_labels),
+            SpvBool(true),
+            0x15u32,
+            *self.look.heading_widths[HeadingRegion::Columns].start() as i32,
+            *self.look.heading_widths[HeadingRegion::Columns].end() as i32,
+            *self.look.heading_widths[HeadingRegion::Rows].start() as i32,
+            *self.look.heading_widths[HeadingRegion::Rows].end() as i32,
+        )
+            .write_le(writer)?;
 
         // Titles.
         (
@@ -170,8 +207,7 @@ impl BinWrite for PivotTable {
         borders_start.finish_le32(writer)?;
 
         // Print Settings.
-        let ps_start = Count::new(writer)?;
-        (
+        Counted::new((
             1u32,
             SpvBool(self.look.print_all_layers),
             SpvBool(self.look.paginate_layers),
@@ -181,14 +217,157 @@ impl BinWrite for PivotTable {
             SpvBool(self.look.bottom_continuation),
             self.look.n_orphan_lines as u32,
             SpvString(self.look.continuation.as_ref().map_or("", |s| s.as_str())),
-        )
-            .write_be(writer)?;
-        ps_start.finish_le32(writer)?;
+        ))
+        .with_endian(Endian::Little)
+        .write_be(writer)?;
 
         // Table Settings.
-        let ts_start = Count::new(writer)?;
-        (1u32, 4u32, self.spv_layer() as u32).write_be(writer)?;
-        ts_start.finish_le32(writer)?;
+        Counted::new((
+            1u32,
+            4u32,
+            self.spv_layer() as u32,
+            SpvBool(self.look.omit_empty),
+            SpvBool(self.look.row_label_position == LabelPosition::Corner),
+            SpvBool(self.look.footnote_marker_type == FootnoteMarkerType::Alphabetic),
+            SpvBool(self.look.footnote_marker_position == FootnoteMarkerPosition::Superscript),
+            0u8,
+            Counted::new((
+                0u32, // n-row-breaks
+                0u32, // n-column-breaks
+                0u32, // n-row-keeps
+                0u32, // n-column-keeps
+                0u32, // n-row-point-keeps
+                0u32, // n-column-point-keeps
+            )),
+            SpvString::optional(&self.notes),
+            SpvString::optional(&self.look.name),
+            Zeros(82),
+        ))
+        .with_endian(Endian::Little)
+        .write_be(writer)?;
+
+        fn y0(pivot_table: &PivotTable) -> impl for<'a> BinWrite<Args<'a> = ()> {
+            (
+                pivot_table.settings.epoch.0 as u32,
+                u8::from(pivot_table.settings.decimal),
+                b',',
+            )
+        }
+
+        fn custom_currency(pivot_table: &PivotTable) -> impl for<'a> BinWrite<Args<'a> = ()> {
+            (
+                5,
+                EnumMap::from_fn(|cc| {
+                    SpvString(pivot_table.settings.number_style(Type::CC(cc)).to_string())
+                })
+                .into_array(),
+            )
+        }
+
+        fn x1(pivot_table: &PivotTable) -> impl for<'a> BinWrite<Args<'a> = ()> {
+            (
+                0u8, // x14
+                if pivot_table.show_title { 1u8 } else { 10u8 },
+                0u8, // x16
+                0u8, // lang
+                Show::as_spv(&pivot_table.show_variables),
+                Show::as_spv(&pivot_table.show_values),
+                -1i32, // x18
+                -1i32, // x19
+                Zeros(17),
+                SpvBool(false), // x20
+                SpvBool(pivot_table.show_caption),
+            )
+        }
+
+        fn x2() -> impl for<'a> BinWrite<Args<'a> = ()> {
+            Counted::new((
+                0u32, // n-row-heights
+                0u32, // n-style-maps
+                0u32, // n-styles,
+                0u32,
+            ))
+        }
+
+        fn y1(pivot_table: &PivotTable) -> impl for<'a> BinWrite<Args<'a> = ()> + use<'_> {
+            (
+                SpvString::optional(&pivot_table.command_c),
+                SpvString::optional(&pivot_table.command_local),
+                SpvString::optional(&pivot_table.language),
+                SpvString("UTF-8"),
+                SpvString::optional(&pivot_table.locale),
+                SpvBool(false), // x10
+                SpvBool(pivot_table.settings.leading_zero),
+                SpvBool(true), // x12
+                SpvBool(true), // x13
+                y0(pivot_table),
+            )
+        }
+
+        fn y2(pivot_table: &PivotTable) -> impl for<'a> BinWrite<Args<'a> = ()> {
+            (custom_currency(pivot_table), b'.', SpvBool(false))
+        }
+
+        fn x3(pivot_table: &PivotTable) -> impl for<'a> BinWrite<Args<'a> = ()> + use<'_> {
+            Counted::new((
+                1u8,
+                0u8,
+                4u8, // x21
+                0u8,
+                0u8,
+                0u8,
+                y1(pivot_table),
+                pivot_table.small,
+                1u8,
+                SpvString::optional(&pivot_table.dataset),
+                SpvString::optional(&pivot_table.datafile),
+                0u32,
+                pivot_table
+                    .date
+                    .map_or(0i64, |date| date.and_utc().timestamp()),
+                y2(pivot_table),
+            ))
+        }
+
+        // Formats.
+        (
+            0u32,
+            SpvString("en_US.ISO_8859-1:1987"),
+            0u32,
+            SpvBool(false),
+            SpvBool(false),
+            SpvBool(true),
+            y0(self),
+            custom_currency(self),
+            Counted::new(Counted::new(((x1(self), x2()), x3(self)))),
+        )
+            .write_le(writer)?;
+
+        // Dimensions.
+        (self.dimensions.len() as u32).write_le(writer)?;
+
+        let x2 = repeat_n(2, self.axes[Axis3::Z].dimensions.len())
+            .chain(repeat_n(0, self.axes[Axis3::Y].dimensions.len()))
+            .chain(repeat(1));
+        for ((index, dimension), x2) in self.dimensions.iter().enumerate().zip(x2) {
+            dimension.write_options(writer, endian, (index, x2))?;
+        }
+
+        // Axes.
+        for axis in [Axis3::Z, Axis3::Y, Axis3::X] {
+            (self.axes[axis].dimensions.len() as u32).write_le(writer)?;
+        }
+        for axis in [Axis3::Z, Axis3::Y, Axis3::X] {
+            for index in self.axes[axis].dimensions.iter().copied() {
+                (index as u32).write_le(writer)?;
+            }
+        }
+
+        // Cells.
+        (self.cells.len() as u32).write_le(writer)?;
+        for (index, value) in &self.cells {
+            (*index as u64, value).write_le(writer)?;
+        }
 
         Ok(())
     }
@@ -217,19 +396,7 @@ where
     }
 
     fn write(&mut self, item: &Arc<Item>) {
-        match &item.details {
-            super::Details::Chart => todo!(),
-            super::Details::Image => todo!(),
-            super::Details::Group(_items) => todo!(),
-            super::Details::Message(_diagnostic) => todo!(),
-            super::Details::PageBreak => {
-                self.needs_page_break = true;
-                return;
-            }
-            super::Details::Table(pivot_table) => self.write_table(&*item, pivot_table),
-            super::Details::Text(_text) => todo!(),
-        };
-        todo!()
+        if let Some(container) = self.write_item(item) {}
     }
 }
 
@@ -395,6 +562,88 @@ impl Table {
 #[derive(Serialize)]
 struct TableStructure;
 
+impl BinWrite for Dimension {
+    type Args<'a> = (usize, u8);
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: Endian,
+        (index, x2): (usize, u8),
+    ) -> binrw::BinResult<()> {
+        (
+            &self.root.name,
+            0u8, // x1
+            x2,
+            2u32, // x3
+            SpvBool(self.root.show_label),
+            SpvBool(self.hide_all_labels),
+            SpvBool(true),
+            index as u32,
+            self.root.children.len() as u32,
+        )
+            .write_options(writer, endian, ())?;
+
+        let mut data_indexes = self.presentation_order.iter().copied();
+        self.root.write_le(writer, &mut data_indexes)
+    }
+}
+
+impl Category {
+    fn write_le<D, W>(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()>
+    where
+        W: Write + Seek,
+        D: Iterator<Item = usize>,
+    {
+        match self {
+            Category::Group(group) => group.write_le(writer, data_indexes),
+            Category::Leaf(leaf) => leaf.write_le(writer, data_indexes),
+        }
+    }
+}
+
+impl Leaf {
+    fn write_le<D, W>(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()>
+    where
+        W: Write + Seek,
+        D: Iterator<Item = usize>,
+    {
+        (
+            self.name(),
+            0u8,
+            0u8,
+            0u8,
+            2u32,
+            data_indexes.next().unwrap() as u32,
+            0u32,
+        )
+            .write_le(writer)
+    }
+}
+
+impl Group {
+    fn write_le<D, W>(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()>
+    where
+        W: Write + Seek,
+        D: Iterator<Item = usize>,
+    {
+        (
+            self.name(),
+            0u8,
+            0u8,
+            1u8,
+            0u32, // x23
+            -1i32,
+        )
+            .write_le(writer)?;
+
+        for child in &self.children {
+            child.write_le(writer, data_indexes)?;
+        }
+        Ok(())
+    }
+}
+
 impl BinWrite for Footnote {
     type Args<'a> = ();
 
@@ -531,13 +780,16 @@ impl BinWrite for SpvBool {
     }
 }
 
-struct SpvString<'a>(&'a str);
-impl<'a> SpvString<'a> {
+struct SpvString<T>(T);
+impl<'a> SpvString<&'a str> {
     fn optional(s: &'a Option<String>) -> Self {
         Self(s.as_ref().map_or("", |s| s.as_str()))
     }
 }
-impl BinWrite for SpvString<'_> {
+impl<T> BinWrite for SpvString<T>
+where
+    T: AsRef<str>,
+{
     type Args<'a> = ();
 
     fn write_options<W: Write + Seek>(
@@ -546,48 +798,9 @@ impl BinWrite for SpvString<'_> {
         endian: binrw::Endian,
         args: Self::Args<'_>,
     ) -> binrw::BinResult<()> {
-        let length = self.0.len() as u32;
-        (length, self.0.as_bytes()).write_options(writer, endian, args)
-    }
-}
-
-#[derive(BinWrite)]
-#[bw(little)]
-struct Header {
-    #[bw(magic(1u16))]
-    version: u8,
-
-    x0: SpvBool,
-    x1: SpvBool,
-    rotate_inner_column_labels: SpvBool,
-    rotate_outer_row_labels: SpvBool,
-    x2: SpvBool,
-    x3: u32,
-    min_col_heading_width: i32,
-    max_col_heading_width: i32,
-    min_row_heading_width: i32,
-    max_row_heading_width: i32,
-}
-
-impl Header {
-    fn new(pivot_table: &PivotTable) -> Self {
-        Self {
-            version: 3,
-            x0: SpvBool(true),
-            x1: SpvBool(false),
-            rotate_inner_column_labels: SpvBool(pivot_table.rotate_inner_column_labels),
-            rotate_outer_row_labels: SpvBool(pivot_table.rotate_outer_row_labels),
-            x2: SpvBool(true),
-            x3: 0x15,
-            min_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].start()
-                as i32,
-            max_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].end()
-                as i32,
-            min_row_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Rows].start()
-                as i32,
-            max_row_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Rows].end()
-                as i32,
-        }
+        let s = self.0.as_ref();
+        let length = s.len() as u32;
+        (length, s.as_bytes()).write_options(writer, endian, args)
     }
 }
 
@@ -640,6 +853,63 @@ impl Count {
     }
 }
 
+struct Counted<T> {
+    inner: T,
+    endian: Option<Endian>,
+}
+
+impl<T> Counted<T> {
+    fn new(inner: T) -> Self {
+        Self {
+            inner,
+            endian: None,
+        }
+    }
+    fn with_endian(self, endian: Endian) -> Self {
+        Self {
+            inner: self.inner,
+            endian: Some(endian),
+        }
+    }
+}
+
+impl<T> BinWrite for Counted<T>
+where
+    T: BinWrite,
+    for<'a> T: BinWrite<Args<'a> = ()>,
+{
+    type Args<'a> = T::Args<'a>;
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        let start = Count::new(writer)?;
+        self.inner.write_options(writer, endian, args)?;
+        start.finish(writer, self.endian.unwrap_or(endian))
+    }
+}
+
+struct Zeros(usize);
+
+impl BinWrite for Zeros {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        _endian: Endian,
+        _args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        for _ in 0..self.0 {
+            writer.write_all(&[0u8])?;
+        }
+        Ok(())
+    }
+}
+
 #[derive(Default)]
 struct StylePair<'a> {
     font_style: Option<&'a FontStyle>,