From 97061003df78885d2810c31c85d01bb7c434a3a1 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Thu, 16 Oct 2025 09:08:13 -0700 Subject: [PATCH] work --- rust/doc/src/spv/light-detail.md | 67 ++++++++++++++--------- rust/pspp/src/output/spv.rs | 8 +-- rust/pspp/src/output/spv/light.rs | 91 ++++++++++++++++--------------- 3 files changed, 92 insertions(+), 74 deletions(-) diff --git a/rust/doc/src/spv/light-detail.md b/rust/doc/src/spv/light-detail.md index b42b6ebbe6..de82b758e1 100644 --- a/rust/doc/src/spv/light-detail.md +++ b/rust/doc/src/spv/light-detail.md @@ -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. +`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. 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. +`notes-unexpanded` 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. + +`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. 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. diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index aab671e2b3..db70db40f3 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -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 { diff --git a/rust/pspp/src/output/spv/light.rs b/rust/pspp/src/output/spv/light.rs index 22442b1cd1..9430f85404 100644 --- a/rust/pspp/src/output/spv/light.rs +++ b/rust/pspp/src/output/spv/light.rs @@ -119,10 +119,10 @@ impl LightTable { pub fn decode(&self) -> Result { 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>, + v1: Optional>, #[br(if(version == Version::V3))] v3: Option>, } @@ -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> { #[binread] #[br(little)] #[derive(Debug)] -struct X2 { +struct N2 { #[br(parse_with(parse_vec))] row_heights: Vec, #[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, + inner: Optional, y2: Y2, #[br(temp)] - _tail: Optional, + _tail: Optional, } #[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, 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 { -- 2.30.2