work
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 16 Oct 2025 16:08:13 +0000 (09:08 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 16 Oct 2025 16:08:13 +0000 (09:08 -0700)
rust/doc/src/spv/light-detail.md
rust/pspp/src/output/spv.rs
rust/pspp/src/output/spv/light.rs

index b42b6ebbe683a686255475027d73936a0051b3c3..de82b758e13c3825250cae655739541b32711280 100644 (file)
@@ -477,9 +477,11 @@ The `PointKeeps` seem to be generated automatically based on
 user-specified Keeps.  They seems to indicate a conversion from rows or
 columns to pixel or point offsets.
 
-`notes` is a text string that contains user-specified notes.  It is
-displayed when the user hovers the cursor over the table, like text in
-the `title` attribute in HTML.  It is not printed.  It is usually empty.
+<a name="notes">`notes`</a> is a text string that contains
+user-specified notes.  It is displayed when the user hovers the cursor
+over the table, like text in the `title` attribute in HTML.  It is not
+printed.  It is usually empty.  See also
+[`notes-unexpanded`](#notes-unexpanded).
 
 `table-look` is the name of a SPSS "TableLook" table style, such as
 "Default" or "Academic"; it is often empty.
@@ -500,8 +502,8 @@ Formats =>
     Y0
     CustomCurrency
     count(
-      v1(X0?)
-      v3(count(X1 count(X2)) count(X3)))
+      v1(N0?)
+      v3(count(N1 count(N2)) count(N3)))
 Y0 => int32[epoch] byte[decimal] byte[grouping]
 CustomCurrency => int32[n-ccs] string*[n-ccs]
 ```
@@ -534,12 +536,12 @@ Most commonly these are all `-,,,` but other strings occur.
 
 A writer may safely use false for `x7`, `x8`, and `x9`.
 
-### X0
+### N0
 
-`X0` only appears, optionally, in version 1 members.
+`N0` only appears, optionally, in version 1 members.
 
 ```
-X0 => byte*14 Y1 Y2
+N0 => byte*14 Y1 Y2
 Y1 =>
     string[command] string[command-local]
     string[language] string[charset] string[locale]
@@ -565,12 +567,12 @@ missing value.  It is always observed as `.`.
 A writer may safely use false for `x10` and `x17` and true for `x12`
 and `x13`.
 
-### X1
+### N1
 
-`X1` only appears in version 3 members.
+`N1` only appears in version 3 members.
 
 ```
-X1 =>
+N1 =>
     bool[x14]
     byte[show-title]
     bool[x16]
@@ -623,12 +625,12 @@ apparent meanings are:
 A writer may safely use false for `x14`, false for `x16`, 0 for
 `lang`, -1 for `x18` and `x19`, and false for `x20`.
 
-### X2
+### N2
 
-`X2` only appears in version 3 members.
+`N2` only appears in version 3 members.
 
 ```
-X2 =>
+N2 =>
     int32[n-row-heights] int32*[n-row-heights]
     int32[n-style-map] StyleMap*[n-style-map]
     int32[n-styles] StylePair*[n-styles]
@@ -639,9 +641,9 @@ StyleMap => int64[cell-index] int16[style-index]
 If present, `n-row-heights` and the accompanying integers are row
 heights as manually adjusted by the user.
 
-The rest of `X2` specifies styles for data cells.  At first glance
+The rest of `N2` specifies styles for data cells.  At first glance
 this is odd, because each data cell can have its own style embedded as
-part of the data, but in practice `X2` specifies a style for a cell
+part of the data, but in practice `N2` specifies a style for a cell
 only if that cell is empty (and thus does not appear in the data at
 all).  Each `StyleMap` specifies the index of a blank cell, calculated
 the same was as in the [Cells](#cells), along with a 0-based index
@@ -650,16 +652,16 @@ into the accompanying StylePair array.
 A writer may safely omit the optional `i0 i0` inside the
 `count(...)`.
 
-### X3
+### N3
 
-`X3` only appears in version 3 members.
+`N3` only appears in version 3 members.
 
 ```
-X3 =>
+N3 =>
     01 00 byte[x21] 00 00 00
     Y1
     double[small] 01
-    (string[dataset] string[datafile] i0 int32[date] i0)?
+    (string[dataset] string[datafile] string[notes-unexpanded] int32[date] i0)?
     Y2
     (int32[x22] i0 bool[x25]?)?
 ```
@@ -674,10 +676,23 @@ scientific notation from being chosen.)
 e.g. `DataSet1`, and `datafile` the name of the file it was read from,
 e.g. `C:\Users\foo\bar.sav`.  The latter is sometimes the empty string.
 
-`date` is a date, as seconds since the epoch, i.e. since January 1,
-1970.  Pivot tables within an SPV file often have dates a few minutes
-apart, so this is probably a creation date for the table rather than for
-the file.
+<a name="notes-unexpanded">`notes-unexpanded`</a> is a text string
+that contains user-specified notes.  It may contain special variables
+such as `)TITLE` that are expanded before the notes are displayed.
+See [`notes`](#notes) for the expanded version.  This text string is
+often empty even if [`notes`](#notes) is nonempty; `notes-unexpanded`
+has only been observed to be nonempty when it contains variables.
+
+> `notes-unexpanded` could be used to allow the user to edit the notes
+in their original form, but to otherwise display them as expanded at
+the time the file was written.  Note that the expansion might change
+if redone since variables can include the date and time, although the
+pivot table also includes [`date`](#date) for anchoring the date.
+
+<a name="date">`date`</a> is a date, as seconds since the epoch,
+i.e. since January 1, 1970.  Pivot tables within an SPV file often
+have dates a few minutes apart, so this is probably a creation date
+for the table rather than for the file.
 
 Sometimes `dataset`, `datafile`, and `date` are present and other
 times they are absent.  The reader can distinguish by assuming that they
@@ -698,11 +713,11 @@ other optional bytes at the end.
 - `locale` in `Formats` itself.
 
 - `locale` in `Y1` (in version 1, `Y1` is optionally nested inside
-`X0`; in version 3, `Y1` is nested inside `X3`).
+`N0`; in version 3, `Y1` is nested inside `N3`).
 
 - `charset` in version 3, in `Y1`.
 
-- `lang` in X1, in version 3.
+- `lang` in `N1`, in version 3.
 
 `charset`, if present, is a good indication of character encoding, and
 in its absence the encoding suffix on `locale` in `Formats` will work.
index aab671e2b373c27b5430edc4541274a3361dcfda..db70db40f32170ce917e09111fcd4ba9bd4e7680 100644 (file)
@@ -187,6 +187,8 @@ impl Heading {
                             .with_spv_info(SpvInfo::new(structure_member).with_error()),
                         ContainerContent::Object => new_error_item("objects not yet implemented")
                             .with_spv_info(SpvInfo::new(structure_member).with_error()),
+                        ContainerContent::Image => new_error_item("images not yet implemented")
+                            .with_spv_info(SpvInfo::new(structure_member).with_error()),
                         ContainerContent::Tree => new_error_item("trees not yet implemented")
                             .with_spv_info(SpvInfo::new(structure_member).with_error()),
                     };
@@ -281,14 +283,10 @@ enum ContainerContent {
     Graph(Graph),
     Model,
     Object,
-    //Image(Image),
+    Image,
     Tree,
 }
 
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Tree;
-
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Graph {
index 22442b1cd13634f2707567bcaf891d4adf2ed044..9430f85404f6e208fa89b1941a8e7693783e3ae3 100644 (file)
@@ -119,10 +119,10 @@ impl LightTable {
     pub fn decode(&self) -> Result<PivotTable, LightError> {
         let encoding = self.formats.encoding();
 
-        let x1 = self.formats.x1();
-        let x2 = self.formats.x2();
-        let x3 = self.formats.x3();
-        let x3_inner = x3.and_then(|x3| x3.inner.as_ref());
+        let n1 = self.formats.n1();
+        let n2 = self.formats.n2();
+        let n3 = self.formats.n3();
+        let n3_inner = n3.and_then(|n3| n3.inner.as_ref());
         let y1 = self.formats.y1();
         let footnotes = self
             .footnotes
@@ -161,13 +161,13 @@ impl LightTable {
                 rotate_inner_column_labels: self.header.rotate_inner_column_labels,
                 rotate_outer_row_labels: self.header.rotate_outer_row_labels,
                 show_grid_lines: self.borders.show_grid_lines,
-                show_title: x1.map_or(true, |x1| x1.show_title != 10),
-                show_caption: x1.map_or(true, |x1| x1.show_caption),
-                show_values: x1.map_or(None, |x1| x1.show_values),
-                show_variables: x1.map_or(None, |x1| x1.show_variables),
+                show_title: n1.map_or(true, |x1| x1.show_title != 10),
+                show_caption: n1.map_or(true, |x1| x1.show_caption),
+                show_values: n1.map_or(None, |x1| x1.show_values),
+                show_variables: n1.map_or(None, |x1| x1.show_variables),
                 sizing: self.table_settings.sizing.decode(
                     &self.formats.column_widths,
-                    x2.map_or(&[], |x2| &x2.row_heights),
+                    n2.map_or(&[], |x2| &x2.row_heights),
                 ),
                 settings: Settings {
                     epoch: self.formats.y0.epoch(),
@@ -179,7 +179,7 @@ impl LightTable {
                     let grouping = self.formats.y0.grouping;
                     b",.' ".contains(&grouping).then_some(grouping as char)
                 },
-                small: x3.map_or(0.0, |x3| x3.small),
+                small: n3.map_or(0.0, |n3| n3.small),
                 weight_format: Format::F40,
             })
             .with_metadata(PivotTableMetadata {
@@ -187,9 +187,9 @@ impl LightTable {
                 command_c: y1.map(|y1| y1.command.decode(encoding)),
                 language: y1.map(|y1| y1.language.decode(encoding)),
                 locale: y1.map(|y1| y1.locale.decode(encoding)),
-                dataset: x3_inner.and_then(|strings| strings.dataset.decode_optional(encoding)),
-                datafile: x3_inner.and_then(|strings| strings.datafile.decode_optional(encoding)),
-                date: x3_inner.and_then(|inner| {
+                dataset: n3_inner.and_then(|strings| strings.dataset.decode_optional(encoding)),
+                datafile: n3_inner.and_then(|strings| strings.datafile.decode_optional(encoding)),
+                date: n3_inner.and_then(|inner| {
                     if inner.date != 0 {
                         DateTime::from_timestamp(inner.date as i64, 0).map(|dt| dt.naive_utc())
                     } else {
@@ -796,7 +796,7 @@ struct Formats {
     y0: Y0,
     custom_currency: CustomCurrency,
     #[br(if(version == Version::V1))]
-    v1: Optional<Counted<X0>>,
+    v1: Optional<Counted<N0>>,
     #[br(if(version == Version::V3))]
     v3: Option<Counted<FormatsV3>>,
 }
@@ -805,20 +805,20 @@ impl Formats {
     fn y1(&self) -> Option<&Y1> {
         self.v1
             .as_ref()
-            .map(|x0| &x0.y1)
-            .or_else(|| self.v3.as_ref().map(|v3| &v3.x3.y1))
+            .map(|n0| &n0.y1)
+            .or_else(|| self.v3.as_ref().map(|v3| &v3.n3.y1))
     }
 
-    fn x1(&self) -> Option<&X1> {
-        self.v3.as_ref().map(|v3| &v3.x1_x2.x1)
+    fn n1(&self) -> Option<&N1> {
+        self.v3.as_ref().map(|v3| &v3.n1_n2.x1)
     }
 
-    fn x2(&self) -> Option<&X2> {
-        self.v3.as_ref().map(|v3| &v3.x1_x2.x2)
+    fn n2(&self) -> Option<&N2> {
+        self.v3.as_ref().map(|v3| &v3.n1_n2.x2)
     }
 
-    fn x3(&self) -> Option<&X3> {
-        self.v3.as_ref().map(|v3| &v3.x3)
+    fn n3(&self) -> Option<&N3> {
+        self.v3.as_ref().map(|v3| &v3.n3)
     }
 
     fn charset(&self) -> Option<&U32String> {
@@ -847,24 +847,24 @@ impl Formats {
 #[derive(Debug)]
 struct FormatsV3 {
     #[br(parse_with(parse_counted))]
-    x1_x2: X1X2,
+    n1_n2: N1N2,
     #[br(parse_with(parse_counted))]
-    x3: X3,
+    n3: N3,
 }
 
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X1X2 {
-    x1: X1,
+struct N1N2 {
+    x1: N1,
     #[br(parse_with(parse_counted))]
-    x2: X2,
+    x2: N2,
 }
 
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X0 {
+struct N0 {
     #[br(temp)]
     _bytes: [u8; 14],
     y1: Y1,
@@ -904,7 +904,7 @@ struct Y2 {
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X1 {
+struct N1 {
     #[br(temp, parse_with(parse_bool))]
     _x14: bool,
     show_title: u8,
@@ -944,7 +944,7 @@ fn parse_show() -> BinResult<Option<Show>> {
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X2 {
+struct N2 {
     #[br(parse_with(parse_vec))]
     row_heights: Vec<i32>,
     #[br(parse_with(parse_vec))]
@@ -958,7 +958,7 @@ struct X2 {
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X3 {
+struct N3 {
     #[br(temp, magic = b"\x01\0")]
     _x21: u8,
     #[br(magic = b"\0\0\0")]
@@ -966,19 +966,19 @@ struct X3 {
     small: f64,
     #[br(magic = 1u8, temp)]
     _one: (),
-    inner: Optional<X3Inner>,
+    inner: Optional<N3Inner>,
     y2: Y2,
     #[br(temp)]
-    _tail: Optional<X3Tail>,
+    _tail: Optional<N3Tail>,
 }
 
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X3Inner {
+struct N3Inner {
     dataset: U32String,
     datafile: U32String,
-    #[br(magic = 0u32)]
+    notes_unexpanded: U32String,
     date: i32,
     #[br(magic = 0u32, temp)]
     _tail: (),
@@ -987,7 +987,7 @@ struct X3Inner {
 #[binread]
 #[br(little)]
 #[derive(Debug)]
-struct X3Tail {
+struct N3Tail {
     #[br(temp)]
     _x22: i32,
     #[br(temp, assert(_zero == 0))]
@@ -1611,13 +1611,18 @@ impl Axes {
     ) -> Result<Vec<(Axis3, pivot::Dimension)>, LightError> {
         let n = self.layers.len() + self.rows.len() + self.columns.len();
         if n != dimensions.len() {
-            return Err(LightError::WrongAxisCount {
-                expected: dimensions.len(),
-                actual: n,
-                n_layers: self.layers.len(),
-                n_rows: self.rows.len(),
-                n_columns: self.columns.len(),
-            });
+            /* XXX warn
+                return Err(LightError::WrongAxisCount {
+                    expected: dimensions.len(),
+                    actual: n,
+                    n_layers: self.layers.len(),
+                    n_rows: self.rows.len(),
+                    n_columns: self.columns.len(),
+            });*/
+            return Ok(dimensions
+                .into_iter()
+                .map(|dimension| (Axis3::Y, dimension))
+                .collect());
         }
 
         fn axis_dims(axis: Axis3, dimensions: &[u32]) -> impl Iterator<Item = (Axis3, usize)> {