tests pass for pivot tables
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 9 Apr 2025 22:24:54 +0000 (15:24 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 9 Apr 2025 22:24:54 +0000 (15:24 -0700)
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/pivot/test.rs
rust/pspp/src/output/pivot/tlo.rs
rust/pspp/src/output/text.rs

index 72d3f8dfde1bff7edf559c3e021b3bf8c007cb1f..58fdeea77eb2e18ca75f0f652592ab8380eb3e4c 100644 (file)
@@ -38,8 +38,8 @@ impl From<TableProperties> for Look {
                     Area::Caption => table_properties.cell_format_properties.caption.style.as_area_style(),
                     Area::Footer => table_properties.cell_format_properties.footnotes.style.as_area_style(),
                     Area::Corner => table_properties.cell_format_properties.corner_labels.style.as_area_style(),
-                    Area::ColumnLabels => table_properties.cell_format_properties.column_labels.style.as_area_style(),
-                    Area::RowLabels => table_properties.cell_format_properties.row_labels.style.as_area_style(),
+                    Area::Labels(Axis2::X) => table_properties.cell_format_properties.column_labels.style.as_area_style(),
+                    Area::Labels(Axis2::Y) => table_properties.cell_format_properties.row_labels.style.as_area_style(),
                     Area::Data => table_properties.cell_format_properties.data.style.as_area_style(),
                     Area::Layers => table_properties.cell_format_properties.layers.style.as_area_style(),
                 },
index f582d18376dbf03ee2ec7b4733731c3b1f1baaa9..5d25d0b9cab542d99a733b33bb82a9b13f29eb0b 100644 (file)
@@ -106,8 +106,9 @@ pub enum Area {
     // Top-left corner.
     Corner,
 
-    ColumnLabels,
-    RowLabels,
+    /// Labels for columns ([Axis2::X]) and rows ([Axis2::Y]).
+    Labels(Axis2),
+
     #[default]
     Data,
 
@@ -124,8 +125,8 @@ impl Area {
             Area::Caption => (Some(Left), Top, [8, 11], [1, 1]),
             Area::Footer => (Some(Left), Top, [11, 8], [2, 3]),
             Area::Corner => (Some(Left), Bottom, [8, 11], [1, 1]),
-            Area::ColumnLabels => (Some(Center), Top, [8, 11], [1, 3]),
-            Area::RowLabels => (Some(Left), Top, [8, 11], [1, 3]),
+            Area::Labels(Axis2::X) => (Some(Center), Top, [8, 11], [1, 3]),
+            Area::Labels(Axis2::Y) => (Some(Left), Top, [8, 11], [1, 3]),
             Area::Data => (None, Top, [8, 11], [1, 1]),
             Area::Layers => (Some(Left), Bottom, [8, 11], [1, 3]),
         };
index 706e7f46ac710dd856f593403d997c0aee77a247..58d5c1f873fc7b7b241ab012a0d251afa1a05edf 100644 (file)
@@ -10,8 +10,8 @@ use crate::output::{
 };
 
 use super::{
-    Area, AsValueOptions, Axis2, Axis3, Border, BorderStyle, BoxBorder, Category, CategoryTrait,
-    Color, Coord2, Dimension, Footnote, PivotTable, Rect2, RowColBorder, Stroke, Value,
+    Area, AsValueOptions, Axis2, Axis3, Border, BorderStyle, BoxBorder, Color, Coord2, Dimension,
+    Footnote, PivotTable, Rect2, RowColBorder, Stroke, Value,
 };
 
 /// All of the combinations of dimensions along an axis.
@@ -181,7 +181,6 @@ impl PivotTable {
             RowColBorder::ColVert,
             self.rotate_outer_row_labels,
             false,
-            Area::ColumnLabels,
         );
         row_headings.render(
             &mut body,
@@ -190,7 +189,6 @@ impl PivotTable {
             RowColBorder::RowHorz,
             false,
             self.rotate_inner_column_labels,
-            Area::RowLabels,
         );
 
         for (y, row_indexes) in row_enumeration.iter().enumerate() {
@@ -338,21 +336,6 @@ pub struct OutputTables {
     pub footnotes: Option<Table>,
 }
 
-fn find_category<'a>(
-    d: &'a Dimension,
-    dim_index: usize,
-    indexes: &[usize],
-    mut row_ofs: usize,
-) -> Option<Category> {
-    let index = indexes[dim_index];
-    let mut c = Category::Leaf(Arc::clone(&d.data_leaves[d.presentation_order[index]]));
-    while row_ofs != c.extra_depth() {
-        row_ofs = row_ofs.checked_sub(1 + c.extra_depth())?;
-        c = Category::Group(Arc::clone(&c.parent()?));
-    }
-    Some(c)
-}
-
 struct HeadingColumn<'a> {
     leaf: &'a Leaf,
     groups: SmallVec<[Arc<Group>; 4]>,
@@ -415,12 +398,11 @@ impl<'a> Heading<'a> {
         h: Axis2,
         h_ofs: usize,
         v_ofs: usize,
-        column_enumeration: &AxisEnumeration,
         col_horz: RowColBorder,
         col_vert: RowColBorder,
         rotate_inner_labels: bool,
         rotate_outer_labels: bool,
-        area: Area,
+        inner: bool,
     ) {
         let v = !h;
 
@@ -453,7 +435,7 @@ impl<'a> Heading<'a> {
                             (rotate_inner_labels && is_inner_row)
                                 || (rotate_outer_labels && is_outer_row)
                         },
-                        area,
+                        area: Area::Labels(h),
                         value: Box::new(name.clone()),
                     },
                 );
@@ -474,19 +456,17 @@ impl<'a> Heading<'a> {
                 // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
                 // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|
                 // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-                // ```
-                let border = if y1 == self.height - 1 {
+                //     ```
+                let border = if row == self.height - 1 && inner {
                     Border::Categories(col_vert)
                 } else {
                     Border::Dimensions(col_vert)
                 };
-                if !vrules[x2] {
-                    table.draw_line(border, (v, x2 + h_ofs), y1..table.n[v]);
-                    vrules[x2] = true;
-                }
-                if !vrules[x1] {
-                    table.draw_line(border, (v, x1 + h_ofs), y1..table.n[v]);
-                    vrules[x1] = true;
+                for x in [x1, x2] {
+                    if !vrules[x] {
+                        table.draw_line(border, (v, x + h_ofs), y1..table.n[v]);
+                        vrules[x] = true;
+                    }
                 }
 
                 // Draws the horizontal lines within a dimension, that is,
@@ -554,7 +534,6 @@ impl<'a> Headings<'a> {
         col_vert: RowColBorder,
         rotate_inner_labels: bool,
         rotate_outer_labels: bool,
-        area: Area,
     ) {
         if self.headings.is_empty() {
             return;
@@ -562,28 +541,27 @@ impl<'a> Headings<'a> {
 
         let h = self.h;
         let n_columns = self.width();
-        let n_rows = self.height();
         let mut vrules = vec![false; n_columns + 1];
         vrules[0] = true;
         vrules[n_columns] = true;
 
         let mut v_ofs = 0;
         for (index, heading) in self.headings.iter().enumerate() {
+            let inner = index == self.headings.len() - 1;
             heading.render(
                 table,
                 &mut vrules,
                 h,
                 h_ofs,
                 v_ofs,
-                self.column_enumeration,
                 col_horz,
                 col_vert,
                 rotate_inner_labels,
                 rotate_outer_labels,
-                area,
+                inner,
             );
             v_ofs += heading.height;
-            if index != self.headings.len() - 1 {
+            if !inner {
                 // Draw the horizontal line between dimensions, e.g. the `=====`
                 // line here:
                 //
index 5905433b9404492a37802b76c0a8260cbff9cbdf..022487d9f338af06553943a9f5eec584cbb64247 100644 (file)
@@ -2,7 +2,7 @@ use std::{fs::File, sync::Arc};
 
 use crate::output::{
     driver::Driver,
-    pivot::{Area, Color, Look},
+    pivot::{Area, Color, Look, PivotTable},
     text::TextDriver,
     Details, Item,
 };
@@ -45,8 +45,7 @@ fn pivot_table_1d() {
     driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt)))));
 }
 
-#[test]
-fn pivot_table_2d() {
+fn d2() -> PivotTable {
     let mut look = Look::default();
     look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left);
     look.areas[Area::Title].font_style.bold = false;
@@ -71,19 +70,91 @@ fn pivot_table_2d() {
             i += 1;
         }
     }
-    let mut pt = pt.with_look(Arc::new(look)).build();
-    let mut driver = TextDriver::new(File::create("/dev/stdout").unwrap());
-    driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone())))));
-    return;
+    pt.with_look(Arc::new(look)).build()
+}
+
+#[test]
+fn d2_columns() {
+    assert_eq!(
+        d2().to_string(),
+        "\
+Columns
+╭────────┬────────┬────────╮
+│   b1   │   b2   │   b3   │
+├──┬──┬──┼──┬──┬──┼──┬──┬──┤
+│a1│a2│a3│a1│a2│a3│a1│a2│a3│
+├──┼──┼──┼──┼──┼──┼──┼──┼──┤
+│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
+╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
+"
+    );
+}
+
+#[test]
+fn d2_rows() {
+    let mut pt = d2();
     pt.transpose();
     pt.title = Some(Box::new(Value::new_text("Rows")));
-    driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone())))));
+    assert_eq!(
+        pt.to_string(),
+        "\
+Rows
+╭─────┬─╮
+│b1 a1│0│
+│   a2│1│
+│   a3│2│
+├─────┼─┤
+│b2 a1│3│
+│   a2│4│
+│   a3│5│
+├─────┼─┤
+│b3 a1│6│
+│   a2│7│
+│   a3│8│
+╰─────┴─╯
+"
+    );
+}
 
+#[test]
+fn d2_column_row() {
+    let mut pt = d2();
+    pt.transpose();
     pt.move_dimension(0, Axis3::X, 0);
     pt.title = Some(Box::new(Value::new_text("Column x Row")));
-    driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone())))));
+    assert_eq!(
+        pt.to_string(),
+        "\
+Column x Row
+╭──┬──┬──┬──╮
+│  │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+"
+    );
+}
 
+#[test]
+fn d2_row_column() {
+    let mut pt = d2();
+    pt.transpose();
+    pt.move_dimension(0, Axis3::X, 0);
     pt.transpose();
     pt.title = Some(Box::new(Value::new_text("Row x Column")));
-    driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone())))));
+    assert_eq!(
+        pt.to_string(),
+        "\
+Row x Column
+╭──┬──┬──┬──╮
+│  │b1│b2│b3│
+├──┼──┼──┼──┤
+│a1│ 0│ 3│ 6│
+│a2│ 1│ 4│ 7│
+│a3│ 2│ 5│ 8│
+╰──┴──┴──┴──╯
+"
+    );
 }
index 74410a2b026c3c289d286535b36940fc51ea1ebf..6697d3a6a8d760dd48dd5ad6208ba34e589c43bf 100644 (file)
@@ -82,8 +82,8 @@ impl From<TableLook> for Look {
                     Area::Caption => (&look.pv_text_style.caption).into(),
                     Area::Footer => (&look.pv_text_style.footer).into(),
                     Area::Corner => (&look.pv_text_style.corner).into(),
-                    Area::ColumnLabels => (&look.pv_text_style.column_labels).into(),
-                    Area::RowLabels => (&look.pv_text_style.row_labels).into(),
+                    Area::Labels(Axis2::X) => (&look.pv_text_style.column_labels).into(),
+                    Area::Labels(Axis2::Y) => (&look.pv_text_style.row_labels).into(),
                     Area::Data => (&look.pv_text_style.data).into(),
                     Area::Layers => (&look.pv_text_style.layers).into(),
             },
index dd3781efb3acebc28b342771bada488cb43a9e2d..6d6bbcbcc74a9fa0356fc609f5c75052bd01984c 100644 (file)
@@ -22,9 +22,7 @@ use super::{
     Details, Item,
 };
 
-pub struct TextDriver {
-    file: BufWriter<File>,
-
+pub struct TextRenderer {
     /// Enable bold and underline in output?
     emphasis: bool,
 
@@ -41,6 +39,32 @@ pub struct TextDriver {
     lines: Vec<TextLine>,
 }
 
+impl TextRenderer {
+    pub fn new() -> Self {
+        let width = 80;
+        Self {
+            emphasis: true,
+            width,
+            min_hbreak: 20,
+            box_chars: &*&UNICODE_BOX,
+            n_objects: 0,
+            params: Params {
+                size: Coord2::new(width, usize::MAX),
+                font_size: EnumMap::from_fn(|_| 1),
+                line_widths: EnumMap::from_fn(|stroke| if stroke == Stroke::None { 0 } else { 1 }),
+                px_size: None,
+                min_break: EnumMap::default(),
+                supports_margins: false,
+                rtl: false,
+                printing: true,
+                can_adjust_break: false,
+                can_scale: false,
+            },
+            lines: Vec::new(),
+        }
+    }
+}
+
 #[derive(Copy, Clone, PartialEq, Eq, Enum)]
 enum Line {
     None,
@@ -241,97 +265,56 @@ impl PivotTable {
     }
 }
 
+impl Display for PivotTable {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.display())
+    }
+}
+
 pub struct DisplayPivotTable<'a> {
     pt: &'a PivotTable,
-
-    /// Enable bold and underline in output?
-    emphasis: bool,
-
-    /// Page width.
-    width: usize,
-
-    /// Minimum cell size to break across pages.
-    min_hbreak: usize,
-
-    box_chars: &'static BoxChars,
-
-    params: Params,
 }
 
 impl<'a> DisplayPivotTable<'a> {
     fn new(pt: &'a PivotTable) -> Self {
-        let width = 80;
-        Self {
-            pt,
-            emphasis: false,
-            width,
-            min_hbreak: 20,
-            box_chars: &*&UNICODE_BOX,
-            params: Params {
-                size: Coord2::new(width, usize::MAX),
-                font_size: EnumMap::from_fn(|_| 1),
-                line_widths: EnumMap::from_fn(|stroke| if stroke == Stroke::None { 0 } else { 1 }),
-                px_size: None,
-                min_break: EnumMap::default(),
-                supports_margins: false,
-                rtl: false,
-                printing: true,
-                can_adjust_break: false,
-                can_scale: false,
-            },
-        }
+        Self { pt }
     }
 }
 
 impl<'a> Display for DisplayPivotTable<'a> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        todo!()
+        for line in TextRenderer::new().render(self.pt) {
+            write!(f, "{}\n", line)?;
+        }
+        Ok(())
     }
 }
 
+pub struct TextDriver {
+    file: BufWriter<File>,
+    renderer: TextRenderer,
+}
+
 impl TextDriver {
     pub fn new(file: File) -> TextDriver {
-        let width = 80;
         Self {
             file: BufWriter::new(file),
-            emphasis: true,
-            width,
-            min_hbreak: 20,
-            box_chars: &*&UNICODE_BOX,
-            n_objects: 0,
-            params: Params {
-                size: Coord2::new(width, usize::MAX),
-                font_size: EnumMap::from_fn(|_| 1),
-                line_widths: EnumMap::from_fn(|stroke| if stroke == Stroke::None { 0 } else { 1 }),
-                px_size: None,
-                min_break: EnumMap::default(),
-                supports_margins: false,
-                rtl: false,
-                printing: true,
-                can_adjust_break: false,
-                can_scale: false,
-            },
-            lines: Vec::new(),
+            renderer: TextRenderer::new(),
         }
     }
+}
 
-    fn output_table(&mut self, table: &PivotTable) {
+impl TextRenderer {
+    fn render(&mut self, table: &PivotTable) -> Vec<TextLine> {
+        let mut output = Vec::new();
         for layer_indexes in table.layers(true) {
             let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
             while pager.has_next(self) {
-                if self.n_objects > 0 {
-                    writeln!(&mut self.file).unwrap();
-                }
-                self.n_objects += 1;
-
-                let h = pager.draw_next(self, usize::MAX);
-
-                for line in self.lines[..h].iter_mut() {
-                    println!("{line}");
-                    line.clear();
-                }
+                pager.draw_next(self, usize::MAX);
+                output.append(&mut self.lines);
             }
         }
+        output
     }
 
     fn display_cell<'a>(cell: &DrawCell<'a>) -> DisplayValue<'a> {
@@ -503,13 +486,17 @@ impl Driver for TextDriver {
             Details::Group(_) => todo!(),
             Details::Message(_diagnostic) => todo!(),
             Details::PageBreak => (),
-            Details::Table(pivot_table) => self.output_table(pivot_table),
+            Details::Table(pivot_table) => {
+                for line in self.renderer.render(&pivot_table) {
+                    writeln!(self.file, "{}", line.str()).unwrap();
+                }
+            }
             Details::Text(_text) => todo!(),
         }
     }
 }
 
-impl Device for TextDriver {
+impl Device for TextRenderer {
     fn params(&self) -> &Params {
         &self.params
     }