pivot table text driver working better
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 9 Apr 2025 21:04:04 +0000 (14:04 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 9 Apr 2025 21:04:04 +0000 (14:04 -0700)
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/text.rs
rust/pspp/src/output/text_line.rs

index 7ce93dab3fc80fede41c1d62e7abce0ba7177659..f582d18376dbf03ee2ec7b4733731c3b1f1baaa9 100644 (file)
@@ -386,6 +386,12 @@ pub struct Group {
     pub show_label: Option<LabelPosition>,
 }
 
+impl Group {
+    pub fn parent(&self) -> Option<Arc<Group>> {
+        self.parent.as_ref().map(|parent| parent.upgrade().unwrap())
+    }
+}
+
 #[derive(Clone)]
 pub struct DimensionBuilder {
     axis: Axis3,
@@ -693,6 +699,9 @@ impl Leaf {
             ..self
         }
     }
+    pub fn ancestors(&self) -> impl Iterator<Item = Arc<Group>> {
+        std::iter::successors(self.parent(), |group| group.parent())
+    }
 }
 
 /// Pivot result classes.
@@ -787,7 +796,7 @@ impl CategoryTrait for Leaf {
     }
 
     fn parent(&self) -> Option<Arc<Group>> {
-        self.parent.upgrade()
+        Some(self.parent.upgrade().unwrap())
     }
 }
 
@@ -1067,16 +1076,16 @@ pub enum VertAlign {
 
 #[derive(Clone, Debug)]
 pub struct FontStyle {
-    bold: bool,
-    italic: bool,
-    underline: bool,
-    markup: bool,
-    font: String,
-    fg: [Color; 2],
-    bg: [Color; 2],
+    pub bold: bool,
+    pub italic: bool,
+    pub underline: bool,
+    pub markup: bool,
+    pub font: String,
+    pub fg: [Color; 2],
+    pub bg: [Color; 2],
 
     /// In 1/72" units.
-    size: i32,
+    pub size: i32,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq)]
index f094fcd3b6211d8de712af5f690589c3ea8ddbb7..706e7f46ac710dd856f593403d997c0aee77a247 100644 (file)
@@ -2,10 +2,10 @@ use std::{ops::Range, sync::Arc};
 
 use enum_map::{enum_map, EnumMap};
 use itertools::Itertools;
-use smallvec::{SmallVec, ToSmallVec};
+use smallvec::SmallVec;
 
 use crate::output::{
-    pivot::LabelPosition,
+    pivot::{Group, LabelPosition, Leaf},
     table::{CellInner, Table},
 };
 
@@ -48,13 +48,12 @@ struct AxisEnumerationIter<'a> {
 }
 
 impl<'a> Iterator for AxisEnumerationIter<'a> {
-    type Item = SmallVec<[usize; 4]>;
+    type Item = &'a [usize];
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.position < self.enumeration.indexes.len() {
-            let item = (&self.enumeration.indexes
-                [self.position..self.position + self.enumeration.stride])
-                .to_smallvec();
+            let item =
+                &self.enumeration.indexes[self.position..self.position + self.enumeration.stride];
             self.position += self.enumeration.stride;
             Some(item)
         } else {
@@ -159,12 +158,13 @@ impl PivotTable {
 
     pub fn output_body(&self, layer_indexes: &[usize], printing: bool) -> Table {
         let column_enumeration = self.enumerate_axis(Axis3::X, layer_indexes, self.look.omit_empty);
+        let column_headings = Headings::new(self, Axis2::X, &column_enumeration);
+
         let row_enumeration = self.enumerate_axis(Axis3::Y, layer_indexes, self.look.omit_empty);
+        let row_headings = Headings::new(self, Axis2::Y, &row_enumeration);
+
         let data = Coord2::new(column_enumeration.len(), row_enumeration.len());
-        let stub = Coord2::new(
-            self.axis_label_depth(Axis3::Y),
-            self.axis_label_depth(Axis3::X),
-        );
+        let stub = Coord2::new(row_headings.height(), column_headings.height());
         let n = EnumMap::from_fn(|axis| data[axis] + stub[axis]).into();
         let mut body = Table::new(
             n,
@@ -173,22 +173,19 @@ impl PivotTable {
             self.borders(printing),
             self.as_value_options(),
         );
-        compose_headings(
-            self,
+
+        column_headings.render(
             &mut body,
-            Axis2::X,
-            &column_enumeration,
+            row_headings.height(),
             RowColBorder::ColHorz,
             RowColBorder::ColVert,
             self.rotate_outer_row_labels,
             false,
             Area::ColumnLabels,
         );
-        compose_headings(
-            self,
+        row_headings.render(
             &mut body,
-            Axis2::Y,
-            &row_enumeration,
+            column_headings.height(),
             RowColBorder::RowVert,
             RowColBorder::RowHorz,
             false,
@@ -356,114 +353,87 @@ fn find_category<'a>(
     Some(c)
 }
 
-/// Fills row or column headings into `table`.
-///
-/// This function uses terminology and variable names for column headings, but
-/// it also applies to row headings because it uses variables for the
-/// differences, e.g. when for column headings it would use the H axis, it
-/// instead uses 'h', which is set to H for column headings and V for row
-/// headings.
-fn compose_headings(
-    pt: &PivotTable,
-    table: &mut Table,
-    h: Axis2,
-    column_enumeration: &AxisEnumeration,
-    col_horz: RowColBorder,
-    col_vert: RowColBorder,
-    rotate_inner_labels: bool,
-    rotate_outer_labels: bool,
-    area: Area,
-) {
-    let v = !h;
-    let h_axis = &pt.axes[h.into()];
-    let v_size = pt.axis_label_depth(h.into());
-    let h_ofs = pt.axis_label_depth(v.into());
-    let n_columns = column_enumeration.len();
-
-    if h_axis.dimensions.is_empty() || n_columns == 0 || v_size == 0 {
-        return;
+struct HeadingColumn<'a> {
+    leaf: &'a Leaf,
+    groups: SmallVec<[Arc<Group>; 4]>,
+}
+
+impl<'a> HeadingColumn<'a> {
+    pub fn get(&self, y: usize, height: usize) -> Option<&Value> {
+        if y + 1 == height {
+            Some(&self.leaf.name)
+        } else {
+            self.groups.get(y).map(|group| &*group.name)
+        }
     }
+}
 
-    // Below, we're going to iterate through the dimensions.  Each dimension
-    // occupies one or more rows in the heading.  `top_row` is the top row of
-    // these (and `top_row + d->label_depth - 1` is the bottom row).
-    let mut top_row = 0;
-
-    // We're going to iterate through dimensions and the rows that label them
-    // from top to bottom (from outer to inner dimensions).  As we move
-    // downward, we start drawing vertical rules to separate categories and
-    // groups.  After we start drawing a vertical rule in a particular
-    // horizontal position, it continues until the bottom of the heading.
-    // vrules[pos] indicates whether, in our current row, we have already
-    // started drawing a vertical rule in horizontal position `pos`.  (There are
-    // n_columns + 1 horizontal positions.  We allocate all of them for
-    // convenience below but only the inner `n_columns - 1` of them really
-    // matter.)
-    //
-    // Here's an example that shows how vertical rules continue all the way
-    // downward:
-    //
-    // ```text
-    // +-----------------------------------------------------+ __
-    // |                         bbbb                        |  |
-    // +-----------------+-----------------+-----------------+  |dimension "bbbb"
-    // |      bbbb1      |      bbbb2      |      bbbb3      | _|
-    // +-----------------+-----------------+-----------------+ __
-    // |       aaaa      |       aaaa      |       aaaa      |  |
-    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+  |dimension "aaaa"
-    // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _|
-    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-    //
-    // ^     ^     ^     ^     ^     ^     ^     ^     ^     ^
-    // |     |     |     |     |     |     |     |     |     |
-    // 0     1     2     3     4     5     6     7     8     9
-    // |___________________vrules[] indexes__________________|
-    // ```
-    //
-    // Our data structures are more naturally iterated from bottom to top (inner
-    // to outer dimensions).  A previous version of this code actually worked
-    // like that, but it didn't draw all of the vertical lines correctly as
-    // shown above.  It ended up rendering the above heading much like shown
-    // below, which isn't what users expect.  The "aaaa" label really needs to
-    // be shown three times for clarity:
-    //
-    // ```text
-    // +-----------------------------------------------------+
-    // |                         bbbb                        |
-    // +-----------------+-----------------+-----------------+
-    // |      bbbb1      |      bbbb2      |      bbbb3      |
-    // +-----------------+-----------------+-----------------+
-    // |                 |       aaaa      |                 |
-    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-    // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|
-    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-    // ```
-    let mut vrules = vec![false; n_columns + 1];
-    vrules[0] = true;
-    vrules[n_columns] = true;
-
-    for (dim_index, d) in pt
-        .axis_dimensions(h.into())
-        .enumerate()
-        .rev()
-        .filter(|(_, d)| !d.hide_all_labels)
-    {
-        for row_ofs in 0..d.label_depth() {
+struct Heading<'a> {
+    dimension: &'a Dimension,
+    height: usize,
+    columns: Vec<HeadingColumn<'a>>,
+}
+
+impl<'a> Heading<'a> {
+    fn new(
+        dimension: &'a Dimension,
+        dim_index: usize,
+        column_enumeration: &AxisEnumeration,
+    ) -> Option<Self> {
+        if dimension.hide_all_labels {
+            return None;
+        }
+
+        let mut columns = Vec::new();
+        let mut height = 0;
+        for indexes in column_enumeration.iter() {
+            let leaf = &*dimension.data_leaves[dimension.presentation_order[indexes[dim_index]]];
+            let mut groups = leaf
+                .ancestors()
+                .filter(|group| group.show_label.is_some())
+                .collect::<SmallVec<_>>();
+            groups.reverse();
+            height = height.max(1 + groups.len());
+            columns.push(HeadingColumn { leaf, groups });
+        }
+
+        Some(Self {
+            dimension,
+            height,
+            columns,
+        })
+    }
+
+    fn width(&self) -> usize {
+        self.columns.len()
+    }
+
+    fn render(
+        &self,
+        table: &mut Table,
+        vrules: &mut [bool],
+        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,
+    ) {
+        let v = !h;
+
+        for row in 0..self.height {
             // Find all the categories, dropping columns without a category.
-            let categories = (0..n_columns).filter_map(|x| {
-                find_category(
-                    d,
-                    dim_index,
-                    column_enumeration.get(x),
-                    d.label_depth() - row_ofs - 1,
-                )
-                .map(|c| (x..x + 1, c))
+            let categories = self.columns.iter().enumerate().filter_map(|(x, column)| {
+                column.get(row, self.height).map(|name| (x..x + 1, name))
             });
 
             // Merge adjacent identical categories (but don't merge across a vertical rule).
             let categories = categories
                 .coalesce(|(a_r, a), (b_r, b)| {
-                    if !vrules[b_r.start] && a.ptr_eq(&b) {
+                    if !vrules[b_r.start] && std::ptr::eq(a, b) {
                         Ok((a_r.start..b_r.end, a))
                     } else {
                         Err(((a_r, a), (b_r, b)))
@@ -471,106 +441,191 @@ fn compose_headings(
                 })
                 .collect::<Vec<_>>();
 
-            for (Range { start: x1, end: x2 }, c) in categories {
-                let y1 = top_row + row_ofs;
-                let y2 = y1 + c.extra_depth() + 1;
-                let is_outer_row = y1 == 0;
-                let is_inner_row = y2 == v_size;
-                if c.show_label() {
-                    table.put(
-                        Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2),
-                        CellInner {
-                            rotate: (rotate_inner_labels && is_inner_row)
-                                || (rotate_outer_labels && is_outer_row),
-                            area,
-                            value: Box::new(c.name().clone()),
+            for (Range { start: x1, end: x2 }, name) in categories {
+                let y1 = v_ofs + row;
+                let y2 = y1 + 1;
+                table.put(
+                    Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2),
+                    CellInner {
+                        rotate: {
+                            let is_outer_row = y1 == 0;
+                            let is_inner_row = y2 == self.height;
+                            (rotate_inner_labels && is_inner_row)
+                                || (rotate_outer_labels && is_outer_row)
                         },
-                    );
-
-                    // Draw all the vertical lines in our running example, other
-                    // than the far left and far right ones.  Only the ones that
-                    // start in the last row of the heading are drawn with the
-                    // "category" style, the rest with the "dimension" style,
-                    // e.g. only the # below are category style:
-                    //
-                    // ```text
-                    // +-----------------------------------------------------+
-                    // |                         bbbb                        |
-                    // +-----------------+-----------------+-----------------+
-                    // |      bbbb1      |      bbbb2      |      bbbb3      |
-                    // +-----------------+-----------------+-----------------+
-                    // |       aaaa      |       aaaa      |       aaaa      |
-                    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-                    // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|
-                    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-                    // ```
-                    let border = if y1 == v_size - 1 {
-                        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;
-                    }
+                        area,
+                        value: Box::new(name.clone()),
+                    },
+                );
 
-                    // Draws the horizontal lines within a dimension, that is,
-                    // those that separate a category (or group) from its parent
-                    // group or dimension's label.  Our running example doesn't
-                    // have groups but the `====` lines below show the
-                    // separators between categories and their dimension label:
-                    //
-                    // ```text
-                    // +-----------------------------------------------------+
-                    // |                         bbbb                        |
-                    // +=================+=================+=================+
-                    // |      bbbb1      |      bbbb2      |      bbbb3      |
-                    // +-----------------+-----------------+-----------------+
-                    // |       aaaa      |       aaaa      |       aaaa      |
-                    // +=====+=====+=====+=====+=====+=====+=====+=====+=====+
-                    // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|
-                    // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-                    // ```
-                    if c.parent().is_some_and(|parent| parent.show_label.is_some()) {
-                        table.draw_line(Border::Categories(col_horz), (h, y1), h_ofs..table.n[h]);
-                    }
+                // Draw all the vertical lines in our running example, other
+                // than the far left and far right ones.  Only the ones that
+                // start in the last row of the heading are drawn with the
+                // "category" style, the rest with the "dimension" style,
+                // e.g. only the # below are category style:
+                //
+                // ```text
+                // +-----------------------------------------------------+
+                // |                         bbbb                        |
+                // +-----------------+-----------------+-----------------+
+                // |      bbbb1      |      bbbb2      |      bbbb3      |
+                // +-----------------+-----------------+-----------------+
+                // |       aaaa      |       aaaa      |       aaaa      |
+                // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+                // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|
+                // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+                // ```
+                let border = if y1 == self.height - 1 {
+                    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;
                 }
-            }
 
-            if d.root.show_label == Some(LabelPosition::Corner) && h_ofs > 0 {
-                table.put(
-                    Rect2::for_ranges((h, 0..h_ofs), top_row..top_row + d.label_depth()),
-                    CellInner::new(Area::Corner, d.root.name.clone()),
-                );
+                // Draws the horizontal lines within a dimension, that is,
+                // those that separate a category (or group) from its parent
+                // group or dimension's label.  Our running example doesn't
+                // have groups but the `====` lines below show the
+                // separators between categories and their dimension label:
+                //
+                // ```text
+                // +-----------------------------------------------------+
+                // |                         bbbb                        |
+                // +=================+=================+=================+
+                // |      bbbb1      |      bbbb2      |      bbbb3      |
+                // +-----------------+-----------------+-----------------+
+                // |       aaaa      |       aaaa      |       aaaa      |
+                // +=====+=====+=====+=====+=====+=====+=====+=====+=====+
+                // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|
+                // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+                // ```
+                if row > 0 {
+                    table.draw_line(Border::Categories(col_horz), (h, y1), h_ofs..table.n[h]);
+                }
             }
+        }
+    }
+}
 
-            // Draw the horizontal line between dimensions, e.g. the `=====`
-            // line here:
-            //
-            // ```text
-            // +-----------------------------------------------------+ __
-            // |                         bbbb                        |  |
-            // +-----------------+-----------------+-----------------+  |dim "bbbb"
-            // |      bbbb1      |      bbbb2      |      bbbb3      | _|
-            // +=================+=================+=================+ __
-            // |       aaaa      |       aaaa      |       aaaa      |  |
-            // +-----+-----+-----+-----+-----+-----+-----+-----+-----+  |dim "aaaa"
-            // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _|
-            // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
-            // ```
-            if dim_index != h_axis.dimensions.len() - 1 {
-                table.draw_line(
-                    Border::Dimensions(col_horz),
-                    (h, top_row),
-                    h_ofs..table.n[h],
-                );
+struct Headings<'a> {
+    headings: Vec<Heading<'a>>,
+    h: Axis2,
+    column_enumeration: &'a AxisEnumeration,
+}
+
+impl<'a> Headings<'a> {
+    fn new(pt: &'a PivotTable, h: Axis2, column_enumeration: &'a AxisEnumeration) -> Self {
+        Self {
+            headings: pt.axes[h.into()]
+                .dimensions
+                .iter()
+                .copied()
+                .enumerate()
+                .rev()
+                .filter_map(|(axis_index, dim_index)| {
+                    Heading::new(&pt.dimensions[dim_index], axis_index, column_enumeration)
+                })
+                .collect(),
+            h,
+            column_enumeration,
+        }
+    }
+
+    fn height(&self) -> usize {
+        self.headings.iter().map(|h| h.height).sum()
+    }
+
+    fn width(&self) -> usize {
+        self.headings.first().map_or(0, |h| h.width())
+    }
+
+    fn render(
+        &self,
+        table: &mut Table,
+        h_ofs: usize,
+        col_horz: RowColBorder,
+        col_vert: RowColBorder,
+        rotate_inner_labels: bool,
+        rotate_outer_labels: bool,
+        area: Area,
+    ) {
+        if self.headings.is_empty() {
+            return;
+        }
+
+        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() {
+            heading.render(
+                table,
+                &mut vrules,
+                h,
+                h_ofs,
+                v_ofs,
+                self.column_enumeration,
+                col_horz,
+                col_vert,
+                rotate_inner_labels,
+                rotate_outer_labels,
+                area,
+            );
+            v_ofs += heading.height;
+            if index != self.headings.len() - 1 {
+                // Draw the horizontal line between dimensions, e.g. the `=====`
+                // line here:
+                //
+                // ```text
+                // +-----------------------------------------------------+ __
+                // |                         bbbb                        |  |
+                // +-----------------+-----------------+-----------------+  |dim "bbbb"
+                // |      bbbb1      |      bbbb2      |      bbbb3      | _|
+                // +=================+=================+=================+ __
+                // |       aaaa      |       aaaa      |       aaaa      |  |
+                // +-----+-----+-----+-----+-----+-----+-----+-----+-----+  |dim "aaaa"
+                // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _|
+                // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+                // ```
+                table.draw_line(Border::Dimensions(col_horz), (h, v_ofs), h_ofs..table.n[h]);
             }
         }
-        top_row += d.label_depth();
+    }
+}
+
+pub fn try_range<R>(range: R, bounds: std::ops::RangeTo<usize>) -> Option<std::ops::Range<usize>>
+where
+    R: std::ops::RangeBounds<usize>,
+{
+    let len = bounds.end;
+
+    let start = match range.start_bound() {
+        std::ops::Bound::Included(&start) => start,
+        std::ops::Bound::Excluded(start) => start.checked_add(1)?,
+        std::ops::Bound::Unbounded => 0,
+    };
+
+    let end = match range.end_bound() {
+        std::ops::Bound::Included(end) => end.checked_add(1)?,
+        std::ops::Bound::Excluded(&end) => end,
+        std::ops::Bound::Unbounded => len,
+    };
+
+    if start > end || end > len {
+        None
+    } else {
+        Some(std::ops::Range { start, end })
     }
 }
 
index e6079ed0152a8d9a59aa71840b3d16e35e60b26a..5905433b9404492a37802b76c0a8260cbff9cbdf 100644 (file)
@@ -1,6 +1,11 @@
 use std::{fs::File, sync::Arc};
 
-use crate::output::{driver::Driver, pivot::Color, text::TextDriver, Details, Item};
+use crate::output::{
+    driver::Driver,
+    pivot::{Area, Color, Look},
+    text::TextDriver,
+    Details, Item,
+};
 
 use super::{Axis3, DimensionBuilder, GroupBuilder, PivotTableBuilder, Value};
 
@@ -42,13 +47,17 @@ fn pivot_table_1d() {
 
 #[test]
 fn pivot_table_2d() {
-    let mut a = GroupBuilder::new(Value::new_text("a")).with_label_shown();
+    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;
+
+    let mut a = GroupBuilder::new(Value::new_text("a"));
     for name in ["a1", "a2", "a3"] {
         a.push(Value::new_text(name));
     }
     let d1 = DimensionBuilder::new(Axis3::X, a);
 
-    let mut b = GroupBuilder::new(Value::new_text("b")).with_label_shown();
+    let mut b = GroupBuilder::new(Value::new_text("b"));
     for name in ["b1", "b2", "b3"] {
         b.push(Value::new_text(name));
     }
@@ -56,16 +65,16 @@ fn pivot_table_2d() {
 
     let mut pt = PivotTableBuilder::new(Value::new_text("Columns"), &[d1, d2]);
     let mut i = 0;
-    for a in 0..3 {
-        for b in 0..3 {
+    for b in 0..3 {
+        for a in 0..3 {
             pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
             i += 1;
         }
     }
-    let mut pt = pt.build();
+    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.transpose();
     pt.title = Some(Box::new(Value::new_text("Rows")));
     driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone())))));
index c290050a62f47be5dd970aba25b177ea3d675af0..dd3781efb3acebc28b342771bada488cb43a9e2d 100644 (file)
@@ -1,5 +1,6 @@
 use std::{
     borrow::Cow,
+    fmt::Display,
     fs::File,
     io::{BufWriter, Write},
     ops::{Index, Range},
@@ -10,6 +11,8 @@ use enum_map::{Enum, EnumMap};
 use unicode_linebreak::{linebreaks, BreakOpportunity};
 use unicode_width::UnicodeWidthStr;
 
+use crate::output::text_line::Emphasis;
+
 use super::{
     driver::Driver,
     pivot::{Axis2, BorderStyle, Coord2, DisplayValue, HorzAlign, PivotTable, Rect2, Stroke},
@@ -232,12 +235,66 @@ static UNICODE_BOX: LazyLock<BoxChars> = LazyLock::new(|| {
     unicode_box
 });
 
+impl PivotTable {
+    pub fn display<'a>(&'a self) -> DisplayPivotTable<'a> {
+        DisplayPivotTable::new(self)
+    }
+}
+
+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,
+            },
+        }
+    }
+}
+
+impl<'a> Display for DisplayPivotTable<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        todo!()
+    }
+}
+
 impl TextDriver {
     pub fn new(file: File) -> TextDriver {
         let width = 80;
         Self {
             file: BufWriter::new(file),
-            emphasis: false,
+            emphasis: true,
             width,
             min_hbreak: 20,
             box_chars: &*&UNICODE_BOX,
@@ -528,7 +585,12 @@ impl Device for TextDriver {
                 continue;
             };
 
-            self.get_line(y).put(x, text);
+            let text = if self.emphasis {
+                Emphasis::from(&cell.style.font_style).apply(text)
+            } else {
+                Cow::from(text)
+            };
+            self.get_line(y).put(x, &text);
         }
     }
 
index 713b892b39ad06dca8f6adc80a1b0485857665c1..9a81402280bab063ec0fbf8bd98a53d1de0f4db9 100644 (file)
@@ -7,6 +7,8 @@ use std::{
 
 use unicode_width::UnicodeWidthChar;
 
+use crate::output::pivot::FontStyle;
+
 /// A line of text, encoded in UTF-8, with support functions that properly
 /// handle double-width characters and backspaces.
 ///
@@ -203,11 +205,20 @@ impl<'a> Iterator for Widths<'a> {
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, Sequence)]
-struct Emphasis {
+pub struct Emphasis {
     pub bold: bool,
     pub underline: bool,
 }
 
+impl From<&FontStyle> for Emphasis {
+    fn from(style: &FontStyle) -> Self {
+        Self {
+            bold: style.bold,
+            underline: style.underline,
+        }
+    }
+}
+
 impl Emphasis {
     const fn plain() -> Self {
         Self {
@@ -215,10 +226,10 @@ impl Emphasis {
             underline: false,
         }
     }
-    fn is_plain(&self) -> bool {
+    pub fn is_plain(&self) -> bool {
         *self == Self::plain()
     }
-    fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> {
+    pub fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> {
         if self.is_plain() {
             Cow::from(s)
         } else {