more legacy
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 25 Dec 2025 21:18:42 +0000 (13:18 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 25 Dec 2025 21:18:42 +0000 (13:18 -0800)
22 files changed:
rust/pspp/src/output/drivers/cairo/fsm.rs
rust/pspp/src/output/drivers/csv.rs
rust/pspp/src/output/drivers/html.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/pivot/testdata/d2_cl-all_layers.expected
rust/pspp/src/output/pivot/testdata/d2_cl-layer0.expected
rust/pspp/src/output/pivot/testdata/d2_cl-layer1.expected
rust/pspp/src/output/pivot/testdata/d2_rl-all_layers.expected
rust/pspp/src/output/pivot/testdata/d2_rl-layer0.expected
rust/pspp/src/output/pivot/testdata/d2_rl-layer1.expected
rust/pspp/src/output/pivot/testdata/d3-layer0_0.expected
rust/pspp/src/output/pivot/testdata/d3-layer0_1.expected
rust/pspp/src/output/pivot/testdata/d3-layer1_2.expected
rust/pspp/src/output/pivot/value.rs
rust/pspp/src/output/render.rs
rust/pspp/src/spv/read.rs
rust/pspp/src/spv/read/legacy_xml.rs
rust/pspp/src/spv/read/tests.rs
rust/pspp/src/spv/testdata/legacy5.expected
rust/pspp/src/spv/testdata/legacy6.expected
rust/pspp/src/spv/testdata/legacy9.expected [new file with mode: 0644]
rust/pspp/src/spv/testdata/legacy9.spv [new file with mode: 0644]

index 05075ca1f00691a4b602c656120e056f0ff4fb2d..786c1a3c9e78a823cd12aeeb93f7548ccdefa219 100644 (file)
@@ -332,7 +332,7 @@ impl<'a, 'b> DrawCell<'a, 'b> {
         };
         layout.set_font_description(Some(font));
 
-        let (body_display, suffixes) = self.display().split_suffixes();
+        let (body_display, suffixes) = self.display().split();
         let horz_align = self.horz_align(&body_display);
 
         let (mut body, mut attrs) = if let Some(markup) = body_display.markup() {
index 749e50535a52ea462eac16854f5d611b289f0389..b34214a6cf723b67874a68d754793e0d4a717e2a 100644 (file)
@@ -309,7 +309,9 @@ impl CsvDriver {
         self.start_item();
 
         self.output_table(pt, output.title.as_ref(), Some("Table"))?;
-        self.output_table(pt, output.layers.as_ref(), Some("Layer"))?;
+        for (index, layer) in output.layers.iter().enumerate() {
+            self.output_table(pt, Some(layer), (index == 0).then_some("Layer"))?;
+        }
         self.output_table(pt, Some(&output.body), None)?;
         self.output_table(pt, output.caption.as_ref(), Some("Caption"))?;
         self.output_table(pt, output.footnotes.as_ref(), Some("Footnote"))?;
index 6552ebe8ae490fd6b76d26449081df8e9bc5d526..77b549963bf14f7f0d877db522d6c43cc1fa638f 100644 (file)
@@ -98,16 +98,18 @@ where
                 )?;
             }
 
-            if let Some(layers) = output.layers {
+            if !output.layers.is_empty() {
                 writeln!(&mut self.writer, "<thead>")?;
-                for cell in layers.cells() {
+                for layer in &output.layers {
                     writeln!(&mut self.writer, "<tr>")?;
-                    self.put_cell(
-                        DrawCell::new(cell.inner(), &layers),
-                        CellRect::new(0..output.body.n[Axis2::X], 0..1),
-                        "td",
-                        None,
-                    )?;
+                    for cell in layer.cells() {
+                        self.put_cell(
+                            DrawCell::new(cell.inner(), layer),
+                            CellRect::for_cell(cell.pos),
+                            "td",
+                            None,
+                        )?;
+                    }
                     writeln!(&mut self.writer, "</tr>")?;
                 }
                 writeln!(&mut self.writer, "</thead>")?;
@@ -172,7 +174,7 @@ where
         table: Option<&Table>,
     ) -> std::io::Result<()> {
         write!(&mut self.writer, "<{tag}")?;
-        let (body, suffixes) = cell.display().split_suffixes();
+        let (body, suffixes) = cell.display().split();
 
         let mut style = String::new();
         let horz_align = match cell.horz_align(&body) {
index fa07f4f2c295c2f5653c9420cc266a9d17db029c..cf23ef768c78cbd26c4734142c81bdf4b3ce526d 100644 (file)
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License along with
 // this program.  If not, see <http://www.gnu.org/licenses/>.
 
-use std::{iter::zip, ops::Range, sync::Arc};
+use std::{iter::once, ops::Range, sync::Arc};
 
 use enum_map::{EnumMap, enum_map};
 use itertools::Itertools;
@@ -138,20 +138,20 @@ impl PivotTable {
         }
     }
 
-    fn create_aux_table3<I>(&self, area: Area, rows: I) -> Table
+    fn create_aux_table<I>(&self, area: Area, axis: Axis2, cells: I) -> Table
     where
         I: Iterator<Item = Box<Value>> + ExactSizeIterator,
     {
         let mut table = Table::new(
-            CellPos::new(1, rows.len()),
+            CellPos::for_axis((axis, cells.len()), 1),
             CellPos::new(0, 0),
             self.style.look.areas.clone(),
             self.borders(false),
             self,
         );
-        for (y, row) in rows.enumerate() {
+        for (z, row) in cells.enumerate() {
             table.put(
-                CellRect::for_cell(CellPos::new(0, y)),
+                CellRect::for_cell(CellPos::for_axis((axis, z), 0)),
                 CellInner::new(area, row),
             );
         }
@@ -163,7 +163,7 @@ impl PivotTable {
         I: Iterator<Item = Box<Value>> + ExactSizeIterator,
     {
         if rows.len() > 0 {
-            Some(self.create_aux_table3(area, rows))
+            Some(self.create_aux_table(area, Axis2::Y, rows))
         } else {
             None
         }
@@ -274,44 +274,48 @@ impl PivotTable {
     /// Constructs a [Table] for this `PivotTable`'s title.  Returns `None` if
     /// the table doesn't have a title.
     pub fn output_title(&self) -> Option<Table> {
-        Some(self.create_aux_table3(
+        Some(self.create_aux_table(
             Area::Title,
+            Axis2::Y,
             [self.metadata.title.as_ref()?.clone()].into_iter(),
         ))
     }
 
     /// Constructs a [Table] for this `PivotTable`'s layer values.  Returns
     /// `None` if the table doesn't have layers.
-    pub fn output_layers(&self, layer_indexes: &[usize]) -> Option<Table> {
-        let mut layers = Vec::new();
-        for (dimension, &layer_index) in zip(
-            self.axes[Axis3::Z]
-                .dimensions
-                .iter()
-                .map(|index| &self.dimensions[*index]),
-            layer_indexes,
-        ) {
-            if !dimension.is_empty() {
-                // `\u{2001}` is an "em quad" space, which looks to me like the
-                // space that SPSS uses here.
-                let s = format!(
-                    "{}:\u{2001}{}",
-                    dimension.root.name().display(self),
-                    dimension.nth_leaf(layer_index).unwrap().0.display(self)
-                );
-                layers.push(Box::new(Value::new_user_text(s)));
-            }
-        }
-        layers.reverse();
-
-        self.create_aux_table_if_nonempty(Area::Layers, layers.into_iter())
+    pub fn output_layers(&self, layer_indexes: &[usize]) -> Vec<Table> {
+        self.axes[Axis3::Z]
+            .dimensions
+            .iter()
+            .map(|index| &self.dimensions[*index])
+            .zip(layer_indexes)
+            .rev()
+            .filter(|(dimension, _)| !dimension.is_empty())
+            .map(|(dimension, &layer_index)| {
+                // Append `:` to the name of the dimension, preserving all the styling.
+                let name = dimension.root.name();
+                let text = format!("{}:", name.display(self).without_suffixes());
+                let name = Value::new_user_text(text).with_styling(name.styling.clone());
+
+                self.create_aux_table(
+                    Area::Layers,
+                    Axis2::X,
+                    [
+                        Box::new(name),
+                        dimension.nth_leaf(layer_index).unwrap().0.clone(),
+                    ]
+                    .into_iter(),
+                )
+            })
+            .collect()
     }
 
     /// Constructs a [Table] for this `PivotTable`'s caption.  Returns `None` if
     /// the table doesn't have a caption.
     pub fn output_caption(&self) -> Option<Table> {
-        Some(self.create_aux_table3(
+        Some(self.create_aux_table(
             Area::Caption,
+            Axis2::Y,
             [self.metadata.caption.as_ref()?.clone()].into_iter(),
         ))
     }
@@ -345,14 +349,15 @@ impl PivotTable {
             .flatten();
 
         // Then collect the footnotes from those tables.
-        let tables = [
-            title.as_ref(),
-            layers.as_ref(),
-            Some(&body),
-            caption.as_ref(),
-        ];
-        let footnotes =
-            self.output_footnotes(&self.collect_footnotes(tables.into_iter().flatten()));
+        let title_iter = once(title.as_ref()).flatten();
+        let layers_iter = layers.iter();
+        let body_iter = once(&body);
+        let caption_iter = once(caption.as_ref()).flatten();
+        let tables_iter = title_iter
+            .chain(layers_iter)
+            .chain(body_iter)
+            .chain(caption_iter);
+        let footnotes = self.output_footnotes(&self.collect_footnotes(tables_iter));
 
         OutputTables {
             title,
@@ -401,8 +406,8 @@ impl PivotTable {
 pub struct OutputTables {
     /// Title table, if any.
     pub title: Option<Table>,
-    /// Layers table, if any.
-    pub layers: Option<Table>,
+    /// Layers tables, if any.
+    pub layers: Vec<Table>,
     /// Table body.
     pub body: Table,
     /// Table caption, if any.
index 842bf2702ab459e04ae9361a426264b2466c85f6..0fac24e5d7da885fbcea5f03f6eaa64f103ce6b2 100644 (file)
@@ -1,5 +1,5 @@
 Column (All Layers)
-b:b1
+b: b1
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
@@ -7,7 +7,7 @@ b: b1
 ╰──┴──┴──╯
 
 Column (All Layers)
-b:b2
+b: b2
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
@@ -15,7 +15,7 @@ b: b2
 ╰──┴──┴──╯
 
 Column (All Layers)
-b:b3
+b: b3
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
index 9a3c09891edf054cee5fdd2bdb6264729af9433f..8cb69bd2bc412c90b9f76b3060633b84eb8c5744 100644 (file)
@@ -1,5 +1,5 @@
 Column x b1
-b:b1
+b: b1
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
index 6611a174d564861722626d23078fc90b0a9f192c..f6c5080f1aff02e3d113f46259804c78dbe3142b 100644 (file)
@@ -1,5 +1,5 @@
 Column x b2
-b:b2
+b: b2
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
index 2a83533b22920293d2f2b73f3b573c93f84d5277..72f11c1822cadac07ccdbc24396e03cf0f3053de 100644 (file)
@@ -1,5 +1,5 @@
 Row (All Layers)
-b:b1
+b: b1
 ╭──┬─╮
 │a1│0│
 │a2│1│
@@ -7,7 +7,7 @@ b: b1
 ╰──┴─╯
 
 Row (All Layers)
-b:b2
+b: b2
 ╭──┬─╮
 │a1│3│
 │a2│4│
@@ -15,7 +15,7 @@ b: b2
 ╰──┴─╯
 
 Row (All Layers)
-b:b3
+b: b3
 ╭──┬─╮
 │a1│6│
 │a2│7│
index 843d15dbbcc8897a048c228e038c86cd86362acc..b516020cc792450f57f0689dad6ac087d6796162 100644 (file)
@@ -1,5 +1,5 @@
 Row x b1
-b:b1
+b: b1
 ╭──┬─╮
 │a1│0│
 │a2│1│
index 53ad394014ef93bd7f0099e80603b5719f0b84fe..1ee2079e106c9ba9efc3c0bd0da6e41bfdba7dd4 100644 (file)
@@ -1,5 +1,5 @@
 Row x b2
-b:b2
+b: b2
 ╭──┬─╮
 │a1│3│
 │a2│4│
index 3ddf5ab679ee73cc45005316da4974db55f66a02..3db87b5dd947ce5d19749a371e2df20f4ed603b1 100644 (file)
@@ -1,6 +1,6 @@
 Column x b1 x a1
-b:b1
-a:a1
+b: b1
+a: a1
 ╭──┬──┬──┬──┬──╮
 │c1│c2│c3│c4│c5│
 ├──┼──┼──┼──┼──┤
index 8f4dd26678576a5ca12b3a391845c9c3a0651def..e1d4dafc908d30510ec48aebfe77bc4251b175c7 100644 (file)
@@ -1,6 +1,6 @@
 Column x b2 x a1
-b:b2
-a:a1
+b: b2
+a: a1
 ╭──┬──┬──┬──┬──╮
 │c1│c2│c3│c4│c5│
 ├──┼──┼──┼──┼──┤
index bc679a3ebed7f5bb891e333c648dc118f617ad35..c4248f0a52dfda378ae695629eac6799513aad81 100644 (file)
@@ -1,6 +1,6 @@
 Column x b3 x a2
-b:b3
-a:a2
+b: b3
+a: a2
 ╭──┬──┬──┬──┬──╮
 │c1│c2│c3│c4│c5│
 ├──┼──┼──┼──┼──┤
index 3af7749c784ee96c29ed42209f652cf2dbe88e2f..9127658e661e7290059937395408c21283666b35 100644 (file)
@@ -176,17 +176,7 @@ impl Value {
     /// Constructs a new text `Value` from `s`, which should have been provided
     /// by the user.
     pub fn new_user_text(s: impl Into<String>) -> Self {
-        let s: String = s.into();
-        if s.is_empty() {
-            Self::default()
-        } else {
-            Self::new(ValueInner::Text(TextValue {
-                user_provided: true,
-                localized: s,
-                c: None,
-                id: None,
-            }))
-        }
+        Self::new(ValueInner::new_user_text(s))
     }
 
     /// Constructs a new `Value` from `variable`.
@@ -282,6 +272,16 @@ impl Value {
         self
     }
 
+    pub fn with_footnotes<'a>(
+        mut self,
+        footnotes: impl IntoIterator<Item = &'a Arc<Footnote>>,
+    ) -> Self {
+        for footnote in footnotes {
+            self.add_footnote(footnote);
+        }
+        self
+    }
+
     /// Adds `footnote` to this `Value`.
     pub fn add_footnote(&mut self, footnote: &Arc<Footnote>) {
         let footnotes = &mut self.styling_mut().footnotes;
@@ -289,6 +289,25 @@ impl Value {
         footnotes.sort_by_key(|f| f.index);
     }
 
+    pub fn with_subscripts<'a>(
+        mut self,
+        subscripts: impl IntoIterator<Item = impl Into<String>>,
+    ) -> Self {
+        for subscript in subscripts {
+            self.add_subscript(subscript);
+        }
+        self
+    }
+
+    pub fn with_subscript(mut self, subscript: impl Into<String>) -> Self {
+        self.add_subscript(subscript);
+        self
+    }
+
+    pub fn add_subscript(&mut self, subscript: impl Into<String>) {
+        self.styling_mut().subscripts.push(subscript.into());
+    }
+
     /// Returns this `Value` with `show` as the [Show] setting for value labels,
     /// if this is a [DatumValue].
     pub fn with_show_value_label(mut self, show: Option<Show>) -> Self {
@@ -498,7 +517,7 @@ impl<'a> DisplayValue<'a> {
 
     /// Returns this display split into `(body, suffixes)` where `suffixes` is
     /// subscripts and footnotes and `body` is everything else.
-    pub fn split_suffixes(self) -> (Self, Self) {
+    pub fn split(self) -> (Self, Self) {
         (self.clone().without_suffixes(), self.without_body())
     }
 
@@ -1182,6 +1201,20 @@ impl ValueInner {
             show_label,
         }
     }
+
+    pub fn new_user_text(s: impl Into<String>) -> Self {
+        let s: String = s.into();
+        if !s.is_empty() {
+            Self::Text(TextValue {
+                user_provided: true,
+                localized: s,
+                c: None,
+                id: None,
+            })
+        } else {
+            Self::Empty
+        }
+    }
 }
 
 /// Styling inside a [Value].
index be194a7dcfc7a849058503716764aeaacf62012d..2f0509e055e1095513897775e9cdb001ef3b8585 100644 (file)
@@ -219,7 +219,7 @@ impl RenderedTable {
     /// The new [Page] will be suitable for rendering on a device whose page
     /// size is `params.size`, but the caller is responsible for actually
     /// breaking it up to fit on such a device, using the [Break] abstraction.
-    fn new(table: Table, device: &dyn Device, min_width: isize, look: &Look) -> Self {
+    fn new(table: Table, device: &dyn Device, min_width: Option<isize>, look: &Look) -> Self {
         use Axis2::*;
         use Extreme::*;
 
@@ -288,7 +288,9 @@ impl RenderedTable {
                 );
             }
         }
-        if min_width > 0 {
+        if let Some(min_width) = min_width
+            && min_width > 0
+        {
             for ext in [Min, Max] {
                 distribute_spanned_width(
                     min_width,
@@ -506,7 +508,7 @@ impl Page {
     /// The new [Page] will be suitable for rendering on a device whose page
     /// size is `params.size`, but the caller is responsible for actually
     /// breaking it up to fit on such a device, using the [Break] abstraction.
-    pub fn new(table: Table, device: &dyn Device, min_width: isize, look: &Look) -> Self {
+    pub fn new(table: Table, device: &dyn Device, min_width: Option<isize>, look: &Look) -> Self {
         let table = Arc::new(RenderedTable::new(table, device, min_width, look));
         let ranges = EnumMap::from_fn(|axis| {
             table.cp[axis][1 + table.h()[axis] * 2]..table.cp[axis].last().copied().unwrap()
@@ -1005,7 +1007,7 @@ impl Pager {
 
         // Figure out the width of the body of the table. Use this to determine
         // the base scale.
-        let body_page = Page::new(output.body, device, 0, &pivot_table.style.look);
+        let body_page = Page::new(output.body, device, None, &pivot_table.style.look);
         let body_width = body_page.width(Axis2::X).min(device.params().size.x());
         let mut scale = if body_width > device.params().size[Axis2::X]
             && pivot_table.style.look.shrink_to_fit[Axis2::X]
@@ -1017,17 +1019,20 @@ impl Pager {
         };
 
         let mut pages = SmallVec::new();
-        for table in [output.title, output.layers].into_iter().flatten() {
+        if let Some(title) = output.title {
             pages.push(Page::new(
-                table,
+                title,
                 device,
-                body_width,
+                Some(body_width),
                 &pivot_table.style.look,
             ));
         }
+        for layer in output.layers {
+            pages.push(Page::new(layer, device, None, &pivot_table.style.look));
+        }
         pages.push(body_page);
         for table in [output.caption, output.footnotes].into_iter().flatten() {
-            pages.push(Page::new(table, device, 0, &pivot_table.style.look));
+            pages.push(Page::new(table, device, None, &pivot_table.style.look));
         }
         pages.reverse();
 
index c12a2ab777ccd96162d4d22517f61d0e36117138..3dfeb4d6693584f228e2386ce983d29c01df4744 100644 (file)
@@ -781,7 +781,6 @@ impl Table {
                     Ok(result) => result,
                     Err(error) => panic!("{error:?}"),
                 };
-                dbg!(&self.table_properties);
                 let pivot_table = visualization.decode(
                     data,
                     self.table_properties
index da6c2914bc8a69a6d08d5e78439106121e99260a..a6c7a1be85e4ff3d7629ef418f8c30af3c2b375f 100644 (file)
@@ -324,7 +324,11 @@ impl Visualization {
             }
         }
         let title = LabelFrame::decode_label(&labels[Purpose::Title], &footnotes);
-        let caption = LabelFrame::decode_label(&labels[Purpose::SubTitle], &footnotes);
+        let mut caption_labels = &labels[Purpose::SubTitle];
+        if caption_labels.is_empty() {
+            caption_labels = &labels[Purpose::Footnote];
+        }
+        let caption = LabelFrame::decode_label(&caption_labels, &footnotes);
         if let Some(style) = &graph.interval.labeling.style
             && let Some(style) = styles.get(style.references.as_str())
         {
@@ -363,7 +367,6 @@ impl Visualization {
             for sv in take(&mut source_variables) {
                 match sv.decode(&data, &series) {
                     Ok(s) => {
-                        dbg!(&sv.id);
                         series.insert(&sv.id, s);
                     }
                     Err(()) => source_variables.push(sv),
@@ -373,7 +376,6 @@ impl Visualization {
             for dv in take(&mut derived_variables) {
                 match dv.decode(&series) {
                     Ok(s) => {
-                        eprintln!("{:?} {:?} {:?}", &dv.id, dv.depends_on, &dv.value);
                         series.insert(&dv.id, s);
                     }
                     Err(()) => derived_variables.push(dv),
@@ -403,7 +405,6 @@ impl Visualization {
             {
                 let mut dimension_style = AreaStyle::default_for_area(Area::Labels(a));
                 let style = label.style.get(&styles);
-                dbg!(variables, label, style);
                 Style::decode_area(
                     style,
                     label.text_frame_style.as_ref().and_then(|r| r.get(styles)),
@@ -431,7 +432,6 @@ impl Visualization {
                 && let Some(gridline) = &axis.major_ticks.gridline
                 && let Some(style) = gridline.style.get(&styles)
             {
-                dbg!(axis, style);
                 if let Some(border_style) = style.border(BoxBorder::Bottom) {
                     // XXX probably not necessary, the Look is supplied at a higher level
                     look.borders[Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))] =
@@ -745,7 +745,7 @@ impl Visualization {
                     for part in s.split(',') {
                         if let Ok(index) = part.parse::<usize>()
                             && let Some(index) = index.checked_sub(1)
-                            && let Some(footnote) = dbg!(footnotes.get(index))
+                            && let Some(footnote) = footnotes.get(index)
                         {
                             value.add_footnote(footnote);
                         }
@@ -1068,7 +1068,7 @@ impl Visualization {
             .collect::<Vec<_>>();
         let mut pivot_table = PivotTable::new(dimensions)
             .with_look(Arc::new(look))
-            .with_footnotes(dbg!(footnotes))
+            .with_footnotes(footnotes)
             .with_data(data)
             .with_layer(&current_layer);
         let decimal = Decimal::for_lang(&self.lang);
@@ -2780,12 +2780,14 @@ impl LabelFrame {
             for t in labels {
                 if let LabelChild::Text(text) = &t.child {
                     for t in text {
-                        if let Some(defines_reference) = t.defines_reference
-                            && let Some(footnote) = footnotes.get(defines_reference.get() - 1)
-                        {
-                            f.push(footnote);
-                        } else {
-                            s += &t.text;
+                        if t.uses_reference.is_none() {
+                            if let Some(defines_reference) = t.defines_reference
+                                && let Some(footnote) = footnotes.get(defines_reference.get() - 1)
+                            {
+                                f.push(footnote);
+                            } else {
+                                s += &t.text;
+                            }
                         }
                     }
                 }
index cf51c110d1af6c6d5c4ba1da33e00f3fc5f8377e..96508810c92f0af0fa3941894af075499da74c55 100644 (file)
@@ -55,6 +55,13 @@ fn legacy8() {
     test_raw_spvfile("legacy8");
 }
 
+/// Checks for caption defined as a footnote label, and for footnotes in layer
+/// values.
+#[test]
+fn legacy9() {
+    test_raw_spvfile("legacy9");
+}
+
 fn test_raw_spvfile(name: &str) {
     let input_filename = Path::new("src/spv/testdata")
         .join(name)
index e4ed57eeb1046a122b2e5041cc15d2c55829dcf9..8b701e2eca076239310fffe31caf9110383f1f65 100644 (file)
@@ -1,5 +1,5 @@
    Statistics
-Variables:Finished
+Variables: Finished
 ╭─────────┬───╮
 │N Valid  │159│
 │  Missing│  0│
index 6f242441181efd075a08ae55a963f827d546cb51..9abd155a085987f74c765d62c714cfe186dd5611 100644 (file)
@@ -1,5 +1,5 @@
  Notes
-Contents:Weight
+Contents: Weight
 ──────
 <none>
 ──────
diff --git a/rust/pspp/src/spv/testdata/legacy9.expected b/rust/pspp/src/spv/testdata/legacy9.expected
new file mode 100644 (file)
index 0000000..9e88b69
--- /dev/null
@@ -0,0 +1,18 @@
+    Analysis
+Test: Duncan[a][b]
+       │ Subset
+SP36  N│   1
+───────┼───────
+3    12│27.4817
+───────┼───────
+2    12│27.4908
+───────┼───────
+1    12│27.5442
+───────┼───────
+Sig.   │   .587
+───────┴───────
+Means for groups in homogeneous subsets are displayed.
+ Based on observed means.
+ The error term is Mean Square(Error) = ,069.
+a. Uses Harmonic Mean Sample Size = 12,000.
+b. Alpha = 0,05.
diff --git a/rust/pspp/src/spv/testdata/legacy9.spv b/rust/pspp/src/spv/testdata/legacy9.spv
new file mode 100644 (file)
index 0000000..11724ae
Binary files /dev/null and b/rust/pspp/src/spv/testdata/legacy9.spv differ