footnotes work
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 11 Apr 2025 22:27:05 +0000 (15:27 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 11 Apr 2025 22:27:05 +0000 (15:27 -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/render.rs
rust/pspp/src/output/table.rs
rust/pspp/src/output/text.rs

index e53858346fefee9ee3cc2067e71be27303e617ce..04a634704b24b86da8c52e1fe70aeaaae096cc69 100644 (file)
@@ -517,7 +517,7 @@ impl From<&str> for CategoryBuilder {
     }
 }
 
-#[derive(Default)]
+#[derive(Clone, Debug, Default)]
 pub struct Footnotes(Vec<Arc<Footnote>>);
 
 impl Footnotes {
@@ -605,6 +605,7 @@ impl PivotTableBuilder {
         table.axes = self.axes;
         table.cells = self.cells;
         table.current_layer = repeat_n(0, table.axes[Axis3::Z].dimensions.len()).collect();
+        table.footnotes = self.footnotes;
         table
     }
 }
@@ -1420,7 +1421,7 @@ pub struct PivotTable {
     pub dataset: Option<String>,
     pub datafile: Option<String>,
     pub date: Option<NaiveDateTime>,
-    pub footnotes: Vec<Arc<Footnote>>,
+    pub footnotes: Footnotes,
     pub title: Option<Box<Value>>,
     pub subtype: Option<Box<Value>>,
     pub corner_text: Option<Box<Value>>,
@@ -1505,7 +1506,7 @@ impl Default for PivotTable {
             dataset: None,
             datafile: None,
             date: None,
-            footnotes: Vec::new(),
+            footnotes: Footnotes::new(),
             subtype: None,
             title: None,
             corner_text: None,
index a7c13c98cc130a5d2dd25e91f9138a33927ce636..84b9d8115eb3062bd369a8a2afc3ac80826f1ae7 100644 (file)
@@ -298,10 +298,11 @@ impl PivotTable {
 
     fn collect_footnotes<'a>(&self, tables: impl Iterator<Item = &'a Table>) -> Vec<Arc<Footnote>> {
         if self.footnotes.is_empty() {
+            dbg!();
             return Vec::new();
         }
 
-        let mut refs = Vec::with_capacity(self.footnotes.len());
+        let mut refs = Vec::with_capacity(self.footnotes.0.len());
         for table in tables {
             for cell in table.cells() {
                 if let Some(styling) = &cell.inner().value.styling {
@@ -309,8 +310,8 @@ impl PivotTable {
                 }
             }
         }
-        refs.sort_by(|a, b| a.index.cmp(&b.index));
-        refs.dedup_by(|a, b| a.index == b.index);
+        refs.sort_by_key(|f| f.index);
+        refs.dedup_by_key(|f| f.index);
         refs
     }
 }
index 8c44dc80247c6b128d65a3e1b001ca8c7d559c91..acc497ddfc4da701d6d43a7194a3cac140237382 100644 (file)
@@ -577,6 +577,7 @@ fn footnotes() {
     );
     let pt = pt
         .with_look(Arc::new(look))
+        .with_footnotes(footnotes)
         .build()
         .with_caption(Value::new_text("Caption").with_footnote(&f0))
         .with_corner_text(
index d5caf49965f5dc37c2386d6523d520025563da32..18cdff5e88fced6e27ec3ec6d325e20206a61468 100644 (file)
@@ -4,7 +4,7 @@ use std::iter::once;
 use std::ops::Range;
 use std::sync::Arc;
 
-use enum_map::EnumMap;
+use enum_map::{enum_map, Enum, EnumMap};
 use itertools::interleave;
 use num::Integer;
 use smallvec::SmallVec;
@@ -79,15 +79,24 @@ impl Params {
     }
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum)]
+pub enum Extreme {
+    Min,
+    Max,
+}
+
 pub trait Device {
     fn params(&self) -> &Params;
 
-    /// Measures `cell`'s width.  Returns an arary `[min_width, max_width]`,
-    /// where `min_width` is the minimum width required to avoid splitting a
-    /// single word across multiple lines (normally, this is the width of the
-    /// longest word in the cell) and `max_width` is the minimum width required
-    /// to avoid line breaks other than at new-lines.
-    fn measure_cell_width(&self, cell: &DrawCell) -> [usize; 2];
+    /// Measures `cell`'s width.  Returns `map`, where:
+    ///
+    /// - `map[Extreme::Min]` is the minimum width required to avoid splitting a
+    ///   single word across multiple lines.  This is usually the width of the
+    ///   longest word in the cell.
+    ///
+    /// - `map[Extreme::Max]` is the minimum width required to avoid line breaks
+    ///   other than at new-lines.
+    fn measure_cell_width(&self, cell: &DrawCell) -> EnumMap<Extreme, usize>;
 
     /// Returns the height required to render `cell` given a width of `width`.
     fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize;
@@ -373,6 +382,7 @@ impl Page {
     /// breaking it up to fit on such a device, using the [Break] abstraction.
     fn new(table: Arc<Table>, device: &dyn Device, min_width: usize, look: &Look) -> Self {
         use Axis2::*;
+        use Extreme::*;
 
         let n = table.n;
 
@@ -387,37 +397,39 @@ impl Page {
         });
 
         let px_size = device.params().px_size.unwrap_or_default();
-        let heading_widths = look
-            .heading_widths
-            .clone()
-            .map(|_region, range| [*range.start() * px_size, *range.end() * px_size]);
+        let heading_widths = look.heading_widths.clone().map(|_region, range| {
+            enum_map![
+                Min => *range.start() * px_size,
+                Max => *range.end() * px_size
+            ]
+        });
 
         // Calculate minimum and maximum widths of cells that do not span
         // multiple columns.
-        let mut unspanned_columns = [vec![0; n.x()], vec![0; n.x()]];
+        let mut unspanned_columns = EnumMap::from_fn(|_| vec![0; n.x()]);
         for cell in table.cells().filter(|cell| cell.col_span() == 1) {
             let mut w = device.measure_cell_width(&DrawCell::new(cell.inner(), &table));
             if device.params().px_size.is_some() {
                 if let Some(region) = table.heading_region(cell.coord) {
                     let wr = &heading_widths[region];
-                    if w[0] < wr[0] {
-                        w[0] = wr[0];
-                        if w[0] > w[1] {
-                            w[1] = w[0];
+                    if w[Min] < wr[Min] {
+                        w[Min] = wr[Min];
+                        if w[Min] > w[Max] {
+                            w[Max] = w[Min];
                         }
-                    } else if w[1] > wr[1] {
-                        w[1] = wr[1];
-                        if w[1] < w[0] {
-                            w[0] = w[1];
+                    } else if w[Max] > wr[Max] {
+                        w[Max] = wr[Max];
+                        if w[Max] < w[Min] {
+                            w[Min] = w[Max];
                         }
                     }
                 }
             }
 
             let x = cell.coord[X];
-            for i in 0..2 {
-                if unspanned_columns[i][x] < w[i] {
-                    unspanned_columns[i][x] = w[i];
+            for ext in [Min, Max] {
+                if unspanned_columns[ext][x] < w[ext] {
+                    unspanned_columns[ext][x] = w[ext];
                 }
             }
         }
@@ -429,21 +441,24 @@ impl Page {
             let rect = cell.rect();
 
             let w = device.measure_cell_width(&DrawCell::new(cell.inner(), &table));
-            for i in 0..2 {
+            for ext in [Min, Max] {
+                dbg!(&cell, ext, w[ext]);
                 distribute_spanned_width(
-                    w[i],
-                    &unspanned_columns[i][rect[X].clone()],
-                    &mut columns[i][rect[X].clone()],
-                    &rules[X][rect[X].start..rect[X].end + 1],
+                    w[ext],
+                    dbg!(&unspanned_columns[ext][rect[X].clone()]),
+                    dbg!(&mut columns[ext][rect[X].clone()]),
+                    dbg!(&rules[X][rect[X].start..rect[X].end + 1]),
                 );
+                dbg!(&mut columns[ext][rect[X].clone()]);
+                eprintln!();
             }
         }
         if min_width > 0 {
-            for i in 0..2 {
+            for ext in [Min, Max] {
                 distribute_spanned_width(
                     min_width,
-                    &unspanned_columns[i],
-                    &mut columns[i],
+                    &unspanned_columns[ext],
+                    &mut columns[ext],
                     &rules[X],
                 );
             }
@@ -454,36 +469,33 @@ impl Page {
         // to exceed the maximum width.  This bollixes our interpolation
         // algorithm later, so fix it up.
         for i in 0..n.x() {
-            if columns[0][i] > columns[1][i] {
-                columns[1][i] = columns[0][i];
+            if columns[Min][i] > columns[Max][i] {
+                columns[Max][i] = columns[Min][i];
             }
         }
 
         // Decide final column widths.
         let rule_widths = rules[X].iter().copied().sum::<usize>();
-        let table_widths = columns
-            .iter()
-            .map(|row| row.iter().sum::<usize>() + rule_widths)
-            .collect::<SmallVec<[usize; 2]>>();
+        let table_widths = EnumMap::from_fn(|ext| columns[ext].iter().sum::<usize>() + rule_widths);
 
-        let cp_x = if table_widths[1] <= device.params().size[X] {
+        let cp_x = if table_widths[Max] <= device.params().size[X] {
             // Fits even with maximum widths.  Use them.
-            Self::use_row_widths(&columns[1], &rules[X])
-        } else if table_widths[0] <= device.params().size[X] {
+            Self::use_row_widths(&columns[Max], &rules[X])
+        } else if table_widths[Min] <= device.params().size[X] {
             // Fits with minimum widths, so distribute the leftover space.
             //Self::new_with_interpolated_widths()
             Self::interpolate_row_widths(
                 device.params(),
-                &columns[0],
-                &columns[1],
-                table_widths[0],
-                table_widths[1],
+                &columns[Min],
+                &columns[Max],
+                table_widths[Min],
+                table_widths[Max],
                 &rules[X],
             )
         } else {
             // Doesn't fit even with minimum widths.  Assign minimums for now, and
             // later we can break it horizontally into multiple pages.
-            Self::use_row_widths(&columns[0], &rules[X])
+            Self::use_row_widths(&columns[Min], &rules[X])
         };
 
         // Calculate heights of cells that do not span multiple rows.
@@ -1070,14 +1082,18 @@ fn distribute_spanned_width(
     rules: &[usize],
 ) {
     let n = unspanned.len();
-    debug_assert!(n >= 1);
+    if n == 0 {
+        return;
+    }
+
     debug_assert_eq!(spanned.len(), n);
     debug_assert_eq!(rules.len(), n + 1);
 
     let total_unspanned = unspanned.iter().sum::<usize>()
         + rules
-            .get(1..n - 1)
+            .get(1..n)
             .map_or(0, |rules| rules.iter().copied().sum::<usize>());
+    dbg!(total_unspanned, width);
     if total_unspanned >= width {
         return;
     }
index 501a5e39055a0d362ed0570ec576c7bca0dcdf20..797e72fad316d0b463e140eea216033ae2188696 100644 (file)
@@ -21,7 +21,7 @@ use super::pivot::{
     Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Rect2, Value, ValueOptions,
 };
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct CellRef<'a> {
     pub coord: Coord2,
     pub content: &'a Content,
index cbecc65b2d6c3945bc584b10c4ed91162e92d14d..a47a95b28863132eba84628789ab557c704937af 100644 (file)
@@ -7,11 +7,11 @@ use std::{
     sync::{Arc, LazyLock},
 };
 
-use enum_map::{Enum, EnumMap};
+use enum_map::{enum_map, Enum, EnumMap};
 use unicode_linebreak::{linebreaks, BreakOpportunity};
 use unicode_width::UnicodeWidthStr;
 
-use crate::output::text_line::Emphasis;
+use crate::output::{render::Extreme, text_line::Emphasis};
 
 use super::{
     driver::Driver,
@@ -464,11 +464,12 @@ impl Device for TextRenderer {
         &self.params
     }
 
-    fn measure_cell_width(&self, cell: &DrawCell) -> [usize; 2] {
+    fn measure_cell_width(&self, cell: &DrawCell) -> EnumMap<Extreme, usize> {
         let text = Self::display_cell(cell).to_string();
-        let max_width = self.layout_cell(&text, Rect2::new(0..usize::MAX, 0..usize::MAX));
-        let min_width = self.layout_cell(&text, Rect2::new(0..1, 0..usize::MAX));
-        [min_width.x(), max_width.x()]
+        enum_map![
+            Extreme::Min => self.layout_cell(&text, Rect2::new(0..1, 0..usize::MAX)).x(),
+            Extreme::Max => self.layout_cell(&text, Rect2::new(0..usize::MAX, 0..usize::MAX)).x(),
+        ]
     }
 
     fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize {