work
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 14 Dec 2025 19:44:49 +0000 (11:44 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 14 Dec 2025 19:44:49 +0000 (11:44 -0800)
rust/pspp/src/dictionary.rs
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/look.rs
rust/pspp/src/output/pivot/value.rs
rust/pspp/src/output/render.rs
rust/pspp/src/spv/read/legacy_xml.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/spv/write.rs

index 73ce62700e2944c3ae23e1f18c001becb4c35d87..c65e695b35f0f7531df3ea0f56c93f38b400af52 100644 (file)
@@ -495,7 +495,7 @@ impl Dictionary {
         group.push("Weight");
         match self.weight_var() {
             Some(variable) => values.push(Value::new_variable(variable)),
-            None => values.push(Value::empty()),
+            None => values.push(Value::new_empty()),
         }
 
         group.push("Documents");
index 152e62c489726d075c8b7f296832b288dfcefc9a..f72874ed331b769e60eceefd3010b332c0977312 100644 (file)
@@ -42,6 +42,9 @@
 //!   a category for each dimension to a value, which is commonly a number but
 //!   could also be a variable name or an arbitrary text string.
 
+// Warn about missing docs, but not for items declared with `#[cfg(test)]`.
+#![cfg_attr(not(test), warn(missing_docs))]
+
 use std::{
     collections::HashMap,
     fmt::{Debug, Display},
@@ -120,7 +123,14 @@ pub struct Axis {
     pub dimensions: Vec<usize>,
 }
 
-pub struct AxisIterator {
+/// Iterator over one of the [Axis3] axes in a [PivotTable].
+///
+/// The items for this iterator are the index values for each of the dimensions
+/// along the axis, each along `0..n` where `n` is the number of leaves in the
+/// dimension.
+///
+/// Use [PivotTable::axis_values] to construct an `AxisIterator`.
+struct AxisIterator {
     indexes: SmallVec<[usize; 4]>,
     lengths: SmallVec<[usize; 4]>,
     done: bool,
@@ -149,12 +159,173 @@ impl Iterator for AxisIterator {
 }
 
 impl PivotTable {
+    /// Constructs a new `PivotTable` with the given `dimensions` along the
+    /// specified axes.
+    ///
+    /// The caller should add a title to the pivot table using [with_title] and
+    /// add data with [with_data] or [insert].
+    ///
+    /// [with_title]: Self::with_title
+    /// [with_data]: Self::with_data
+    /// [insert]: Self::insert
+    pub fn new(dimensions: impl IntoIterator<Item = (Axis3, Dimension)>) -> Self {
+        let mut dims = Vec::new();
+        let mut axes = EnumMap::<Axis3, Axis>::default();
+        for (axis, dimension) in dimensions {
+            axes[axis].dimensions.push(dims.len());
+            dims.push(dimension);
+        }
+        Self {
+            style: PivotTableStyle::default().with_look(Settings::global().look.clone()),
+            layer: repeat_n(0, axes[Axis3::Z].dimensions.len()).collect(),
+            axes,
+            dimensions: dims,
+            ..Self::default()
+        }
+    }
+
+    /// Returns this pivot table with the given `title`.
+    ///
+    /// The title is displayed above the pivot table.  Every pivot table should
+    /// have a title.
+    pub fn with_title(mut self, title: impl Into<Value>) -> Self {
+        self.metadata.title = Some(Box::new(title.into()));
+        self.style.show_title = true;
+        self
+    }
+
+    /// Returns this pivot table with the given `caption`.
+    ///
+    /// The caption is displayed below the pivot table.  Captions are optional.
+    pub fn with_caption(mut self, caption: impl Into<Value>) -> Self {
+        self.metadata.caption = Some(Box::new(caption.into()));
+        self.style.show_caption = true;
+        self
+    }
+
+    /// Returns this pivot table with the given `corner_text`.
+    ///
+    /// The corner text is displayed in the top-left corner of the pivot table,
+    /// above the row headings and to the left of the column headings.  The
+    /// space used by corner text can also be used for [Dimension] titles.
+    pub fn with_corner_text(mut self, corner_text: impl Into<Value>) -> Self {
+        self.metadata.corner_text = Some(Box::new(corner_text.into()));
+        self
+    }
+
+    /// Returns this pivot table with the given `footnotes`.
+    pub fn with_footnotes(mut self, footnotes: Footnotes) -> Self {
+        debug_assert!(self.footnotes.is_empty());
+        self.footnotes = footnotes;
+        self
+    }
+
+    /// Returns this pivot table with the given `look`.
     pub fn with_look(self, look: Arc<Look>) -> Self {
         Self {
             style: self.style.with_look(look),
             ..self
         }
     }
+
+    /// Returns this pivot table with the given `style`.
+    pub fn with_style(self, style: PivotTableStyle) -> Self {
+        Self { style, ..self }
+    }
+
+    /// Returns this pivot table with the given `metadata`.
+    pub fn with_metadata(self, metadata: PivotTableMetadata) -> Self {
+        Self { metadata, ..self }
+    }
+
+    /// Returns this pivot table with the given `subtype`.
+    ///
+    /// A subtype is a locale-invariant command ID for the particular kind of
+    /// output that this table represents in the procedure.  This can be the
+    /// same as the command name, e.g. `Frequencies`, or different, e.g. `Case
+    /// Processing Summary`.
+    ///
+    /// `Notes` and `Warnings` are common generic subtypes.
+    pub fn with_subtype(self, subtype: impl Into<Value>) -> Self {
+        Self {
+            metadata: self.metadata.with_subtype(subtype),
+            ..self
+        }
+    }
+
+    /// Returns this pivot table with the given `show_values`.
+    pub fn with_show_values(self, show_values: Option<Show>) -> Self {
+        Self {
+            style: self.style.with_show_values(show_values),
+            ..self
+        }
+    }
+
+    /// Returns this pivot table with the given `show_variables`.
+    pub fn with_show_variables(self, show_variables: Option<Show>) -> Self {
+        Self {
+            style: self.style.with_show_variables(show_variables),
+            ..self
+        }
+    }
+
+    /// Returns this pivot table with the given `show_title`.
+    pub fn with_show_title(self, show_title: bool) -> Self {
+        Self {
+            style: self.style.with_show_title(show_title),
+            ..self
+        }
+    }
+
+    /// Returns this pivot table with the given `show_caption`.
+    pub fn with_show_caption(self, show_caption: bool) -> Self {
+        Self {
+            style: self.style.with_show_caption(show_caption),
+            ..self
+        }
+    }
+
+    /// Returns this pivot table with its [Look] modified to show empty rows and
+    /// columns.
+    pub fn with_show_empty(mut self) -> Self {
+        if self.style.look.hide_empty {
+            self.look_mut().hide_empty = false;
+        }
+        self
+    }
+
+    /// Returns this pivot table with its [Look] modified to hide empty rows and
+    /// columns.
+    pub fn with_hide_empty(mut self) -> Self {
+        if !self.style.look.hide_empty {
+            self.look_mut().hide_empty = true;
+        }
+        self
+    }
+
+    /// Returns this pivot table with the current layer set to `layer` and its
+    /// look modified (if necessary) to print just a single layer.
+    pub fn with_layer(mut self, layer: &[usize]) -> Self {
+        // XXX verify that `layer` is valid
+        debug_assert_eq!(layer.len(), self.layer.len());
+        if self.style.look.print_all_layers {
+            self.style.look_mut().print_all_layers = false;
+        }
+        self.layer.clear();
+        self.layer.extend_from_slice(layer);
+        self
+    }
+
+    /// Returns this pivot table set to print all layers.
+    pub fn with_all_layers(mut self) -> Self {
+        if !self.style.look.print_all_layers {
+            self.look_mut().print_all_layers = true;
+        }
+        self
+    }
+
+    /// Inserts `number` into the cell with the given `data_indexes`, drawing
+    /// its format from `class`.
     pub fn insert_number(&mut self, data_indexes: &[usize], number: Option<f64>, class: Class) {
         let format = match class {
             Class::Other => Settings::global().default_format,
@@ -171,11 +342,7 @@ impl PivotTable {
         self.insert(data_indexes, value);
     }
 
-    pub fn with_footnotes(mut self, footnotes: Footnotes) -> Self {
-        debug_assert!(self.footnotes.is_empty());
-        self.footnotes = footnotes;
-        self
-    }
+    /// Returns an iterator for all the values along `axis`.
     fn axis_values(&self, axis: Axis3) -> AxisIterator {
         AxisIterator {
             indexes: repeat_n(0, self.axes[axis].dimensions.len()).collect(),
@@ -187,125 +354,314 @@ impl PivotTable {
     fn axis_extent(&self, axis: Axis3) -> usize {
         self.axis_dimensions(axis).map(|d| d.len()).product()
     }
-}
-
-/// A dimension.
-///
-/// A [Dimension] identifies the categories associated with a single dimension
-/// within a multidimensional pivot table.
-///
-/// A dimension contains a collection of categories, which are the leaves in a
-/// tree of groups.
-///
-/// (A dimension or a group can contain zero categories, but this is unusual.
-/// If a dimension contains no categories, then its table cannot contain any
-/// data.)
-#[derive(Clone, Debug, Serialize)]
-pub struct Dimension {
-    /// Hierarchy of categories within the dimension.  The groups and categories
-    /// are sorted in the order that should be used for display.  This might be
-    /// different from the original order produced for output if the user
-    /// adjusted it.
-    ///
-    /// The root must always be a group, although it is allowed to have no
-    /// subcategories.
-    pub root: Group,
-
-    /// Ordering of leaves for presentation.
-    ///
-    /// This is a permutation of `0..n` where `n` is the number of leaves.  It
-    /// maps from an index in presentation order to an index in data order.
-    pub presentation_order: Vec<usize>,
-
-    /// Display.
-    pub hide_all_labels: bool,
-}
-
-pub type GroupVec<'a> = SmallVec<[&'a Group; 4]>;
-pub struct Path<'a> {
-    groups: GroupVec<'a>,
-    leaf: &'a Leaf,
-}
-
-pub type IndexVec = SmallVec<[usize; 4]>;
 
-impl Dimension {
-    pub fn new(root: Group) -> Self {
-        Dimension {
-            presentation_order: (0..root.len()).collect(),
-            root,
-            hide_all_labels: false,
-        }
+    /// Returns the indexes for the layer to be printed or display on-screen.
+    pub fn layer(&self) -> &[usize] {
+        &self.layer
     }
 
-    pub fn is_empty(&self) -> bool {
-        self.len() == 0
+    /// Returns the pivot table's dimensions.
+    pub fn dimensions(&self) -> &[Dimension] {
+        &self.dimensions
     }
 
-    /// Returns the number of (leaf) categories in this dimension.
-    pub fn len(&self) -> usize {
-        self.root.len()
+    /// Returns the pivot table's axes.
+    pub fn axes(&self) -> &EnumMap<Axis3, Axis> {
+        &self.axes
     }
 
-    pub fn nth_leaf(&self, index: usize) -> Option<&Leaf> {
-        self.root.nth_leaf(index)
+    /// Returns the pivot table's [Look], for modification.
+    pub fn look_mut(&mut self) -> &mut Look {
+        self.style.look_mut()
     }
 
-    pub fn leaf_path(&self, index: usize) -> Option<Path<'_>> {
-        self.root.leaf_path(index, SmallVec::new())
+    /// Returns a label for the table, which is either its title or a default
+    /// string.
+    pub fn label(&self) -> String {
+        match &self.metadata.title {
+            Some(title) => title.display(self).to_string(),
+            None => String::from("Table"),
+        }
     }
 
-    pub fn index_path(&self, index: usize) -> Option<IndexVec> {
-        self.root.index_path(index, SmallVec::new())
+    /// Returns the table's title, or an empty value if it doesn't have one.
+    pub fn title(&self) -> &Value {
+        match &self.metadata.title {
+            Some(title) => title,
+            None => Value::static_empty(),
+        }
     }
 
-    pub fn with_all_labels_hidden(self) -> Self {
-        Self {
-            hide_all_labels: true,
-            ..self
+    /// Returns the table's subtype, or an empty value if it doesn't have one.
+    pub fn subtype(&self) -> &Value {
+        match &self.metadata.subtype {
+            Some(subtype) => subtype,
+            None => Value::static_empty(),
         }
     }
-}
-
-/// Specifies a [Category] within a [Group].
-#[derive(Copy, Clone, Debug)]
-pub struct CategoryLocator {
-    /// The index of the leaf to start from.
-    pub leaf_index: usize,
 
-    /// The number of times to go up a level from the leaf.  If this category is
-    /// a leaf, this is 0, otherwise it is positive.
-    pub level: usize,
-}
+    /// Returns the `HashMap` for cells.  Indexes in the map can be computed
+    /// with [cell_index](Self::cell_index).
+    pub fn cells(&self) -> &HashMap<usize, Value> {
+        &self.cells
+    }
 
-impl CategoryLocator {
-    pub fn new_leaf(leaf_index: usize) -> Self {
-        Self {
-            leaf_index,
-            level: 0,
-        }
+    /// Computes an index into the `cells` `HashMap` for `cell_index`.
+    pub fn cell_index<C>(&self, cell_index: C) -> usize
+    where
+        C: CellIndex,
+    {
+        cell_index.cell_index(self.dimensions.iter().map(|d| d.len()))
     }
 
-    pub fn parent(&self) -> Self {
-        Self {
-            leaf_index: self.leaf_index,
-            level: self.level + 1,
-        }
+    /// Inserts a cell with the given `value` and `cell_index`.
+    pub fn insert<C>(&mut self, cell_index: C, value: impl Into<Value>)
+    where
+        C: CellIndex,
+    {
+        self.cells.insert(self.cell_index(cell_index), value.into());
     }
 
-    pub fn as_leaf(&self) -> Option<usize> {
-        (self.level == 0).then_some(self.leaf_index)
+    /// Returns the cell with the given `cell_index`, if there is one.
+    pub fn get<C>(&self, cell_index: C) -> Option<&Value>
+    where
+        C: CellIndex,
+    {
+        self.cells.get(&self.cell_index(cell_index))
     }
-}
 
-#[derive(Clone, Debug, Serialize)]
-pub struct Group {
-    #[serde(skip)]
-    len: usize,
-    pub name: Box<Value>,
+    /// Returns the pivot table with cell indexes and values from `iter`
+    /// inserted as data.
+    pub fn with_data<C>(mut self, iter: impl IntoIterator<Item = (C, Value)>) -> Self
+    where
+        C: CellIndex,
+    {
+        self.extend(iter);
+        self
+    }
 
-    /// The child categories.
-    ///
+    /// Converts per-axis presentation-order indexes in `presentation_indexes`,
+    /// into data indexes for each dimension.
+    fn convert_indexes_ptod(
+        &self,
+        presentation_indexes: EnumMap<Axis3, &[usize]>,
+    ) -> SmallVec<[usize; 4]> {
+        let mut data_indexes = SmallVec::from_elem(0, self.dimensions.len());
+        for (axis, presentation_indexes) in presentation_indexes {
+            for (&dim_index, &pindex) in self.axes[axis]
+                .dimensions
+                .iter()
+                .zip(presentation_indexes.iter())
+            {
+                data_indexes[dim_index] = self.dimensions[dim_index].presentation_order[pindex];
+            }
+        }
+        data_indexes
+    }
+
+    /// Returns an iterator for the layer axis:
+    ///
+    /// - If `print` is true and `self.look.print_all_layers`, then the iterator
+    ///   will visit all values of the layer axis.
+    ///
+    /// - Otherwise, the iterator will just visit `self.current_layer`.
+    pub fn layers(&self, print: bool) -> Box<dyn Iterator<Item = SmallVec<[usize; 4]>>> {
+        if print && self.style.look.print_all_layers {
+            Box::new(self.axis_values(Axis3::Z))
+        } else {
+            Box::new(once(SmallVec::from_slice(&self.layer)))
+        }
+    }
+
+    pub fn transpose(&mut self) {
+        self.axes.swap(Axis3::X, Axis3::Y);
+    }
+
+    pub fn axis_dimensions(
+        &self,
+        axis: Axis3,
+    ) -> impl DoubleEndedIterator<Item = &Dimension> + ExactSizeIterator {
+        self.axes[axis]
+            .dimensions
+            .iter()
+            .copied()
+            .map(|index| &self.dimensions[index])
+    }
+
+    fn find_dimension(&self, dim_index: usize) -> Option<(Axis3, usize)> {
+        debug_assert!(dim_index < self.dimensions.len());
+        for axis in enum_iterator::all::<Axis3>() {
+            for (position, dimension) in self.axes[axis].dimensions.iter().copied().enumerate() {
+                if dimension == dim_index {
+                    return Some((axis, position));
+                }
+            }
+        }
+        None
+    }
+    pub fn move_dimension(&mut self, dim_index: usize, new_axis: Axis3, new_position: usize) {
+        let (old_axis, old_position) = self.find_dimension(dim_index).unwrap();
+        if old_axis == new_axis && old_position == new_position {
+            return;
+        }
+
+        // Update the current layer, if necessary.  If we're moving within the
+        // layer axis, preserve the current layer.
+        match (old_axis, new_axis) {
+            (Axis3::Z, Axis3::Z) => {
+                // Rearrange the layer axis.
+                if old_position < new_position {
+                    self.layer[old_position..=new_position].rotate_left(1);
+                } else {
+                    self.layer[new_position..=old_position].rotate_right(1);
+                }
+            }
+            (Axis3::Z, _) => {
+                // A layer is becoming a row or column.
+                self.layer.remove(old_position);
+            }
+            (_, Axis3::Z) => {
+                // A row or column is becoming a layer.
+                self.layer.insert(new_position, 0);
+            }
+            _ => (),
+        }
+
+        self.axes[old_axis].dimensions.remove(old_position);
+        self.axes[new_axis]
+            .dimensions
+            .insert(new_position, dim_index);
+    }
+}
+
+impl IntoValueOptions for &PivotTable {
+    fn into_value_options(self) -> ValueOptions {
+        ValueOptions {
+            show_values: self.style.show_values,
+            show_variables: self.style.show_variables,
+            small: self.style.small,
+            footnote_marker_type: self.style.look.footnote_marker_type,
+        }
+    }
+}
+
+/// A dimension.
+///
+/// A [Dimension] identifies the categories associated with a single dimension
+/// within a multidimensional pivot table.
+///
+/// A dimension contains a collection of categories, which are the leaves in a
+/// tree of groups.
+///
+/// (A dimension or a group can contain zero categories, but this is unusual.
+/// If a dimension contains no categories, then its table cannot contain any
+/// data.)
+#[derive(Clone, Debug, Serialize)]
+pub struct Dimension {
+    /// Hierarchy of categories within the dimension.  The groups and categories
+    /// are sorted in the order that should be used for display.  This might be
+    /// different from the original order produced for output if the user
+    /// adjusted it.
+    ///
+    /// The root must always be a group, although it is allowed to have no
+    /// subcategories.
+    pub root: Group,
+
+    /// Ordering of leaves for presentation.
+    ///
+    /// This is a permutation of `0..n` where `n` is the number of leaves.  It
+    /// maps from an index in presentation order to an index in data order.
+    pub presentation_order: Vec<usize>,
+
+    /// Display.
+    pub hide_all_labels: bool,
+}
+
+pub type GroupVec<'a> = SmallVec<[&'a Group; 4]>;
+pub struct Path<'a> {
+    groups: GroupVec<'a>,
+    leaf: &'a Leaf,
+}
+
+pub type IndexVec = SmallVec<[usize; 4]>;
+
+impl Dimension {
+    pub fn new(root: Group) -> Self {
+        Dimension {
+            presentation_order: (0..root.len()).collect(),
+            root,
+            hide_all_labels: false,
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
+    /// Returns the number of (leaf) categories in this dimension.
+    pub fn len(&self) -> usize {
+        self.root.len()
+    }
+
+    pub fn nth_leaf(&self, index: usize) -> Option<&Leaf> {
+        self.root.nth_leaf(index)
+    }
+
+    pub fn leaf_path(&self, index: usize) -> Option<Path<'_>> {
+        self.root.leaf_path(index, SmallVec::new())
+    }
+
+    pub fn index_path(&self, index: usize) -> Option<IndexVec> {
+        self.root.index_path(index, SmallVec::new())
+    }
+
+    pub fn with_all_labels_hidden(self) -> Self {
+        Self {
+            hide_all_labels: true,
+            ..self
+        }
+    }
+}
+
+/// Specifies a [Category] within a [Group].
+#[derive(Copy, Clone, Debug)]
+pub struct CategoryLocator {
+    /// The index of the leaf to start from.
+    pub leaf_index: usize,
+
+    /// The number of times to go up a level from the leaf.  If this category is
+    /// a leaf, this is 0, otherwise it is positive.
+    pub level: usize,
+}
+
+impl CategoryLocator {
+    pub fn new_leaf(leaf_index: usize) -> Self {
+        Self {
+            leaf_index,
+            level: 0,
+        }
+    }
+
+    pub fn parent(&self) -> Self {
+        Self {
+            leaf_index: self.leaf_index,
+            level: self.level + 1,
+        }
+    }
+
+    pub fn as_leaf(&self) -> Option<usize> {
+        (self.level == 0).then_some(self.leaf_index)
+    }
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Group {
+    #[serde(skip)]
+    len: usize,
+    pub name: Box<Value>,
+
+    /// The child categories.
+    ///
     /// A group usually has multiple children, but it is allowed to have
     /// only one or even (pathologically) none.
     pub children: Vec<Category>,
@@ -447,6 +803,12 @@ where
     }
 }
 
+/// A collection of [Footnote]s for a [PivotTable].
+///
+/// Any [Value] in a pivot table can refer to a footnote.  All of the footnotes
+/// used in any [Value] within a given pivot table must be collected into the
+/// [Footnotes] attached to the pivot table.  (Footnotes used but not collected
+/// might not display as intended, but it's not a safety issue.)
 #[derive(Clone, Debug, Default, Serialize)]
 pub struct Footnotes(Vec<Arc<Footnote>>);
 
@@ -894,28 +1256,42 @@ pub enum FootnoteMarkerPosition {
 }
 
 /// A [Look] and other styling for a [PivotTable].
-///
-/// The division between [Look] and the rest of the styling in this structure is
-/// fairly arbitrary.  The ultimate reason for the division is simply because
-/// that's how SPSS documentation and file formats do it.
 #[derive(Clone, Debug, Serialize)]
 pub struct PivotTableStyle {
     /// The [Look].
+    ///
+    /// The division between [Look] and the rest of the styling in this
+    /// structure is fairly arbitrary.  The ultimate reason for the division is
+    /// simply because that's how SPSS documentation and file formats do it.
     pub look: Arc<Look>,
 
+    /// Display inner column labels as vertical text?
     pub rotate_inner_column_labels: bool,
 
+    /// Display outer row labels as vertical text?
     pub rotate_outer_row_labels: bool,
 
+    /// Show grid lines between data cells?
     pub show_grid_lines: bool,
 
+    /// Display the title?
     pub show_title: bool,
 
+    /// Display the caption?
     pub show_caption: bool,
 
+    /// Default [Show] value for showing values in [Value]s that don't specify
+    /// their own.
+    ///
+    /// If this is `None` then a global default is used.
     pub show_values: Option<Show>,
 
+    /// Default [Show] value for showing variables in [Value]s that don't
+    /// specify their own.
+    ///
+    /// If this is `None` then a global default is used.
     pub show_variables: Option<Show>,
+
     /// Column and row sizing and page breaks:
     ///
     /// - `sizing[Axis2::X]` is sizes for columns.
@@ -928,6 +1304,9 @@ pub struct PivotTableStyle {
     /// Numeric grouping character (usually `.` or `,`).
     pub grouping: Option<char>,
 
+    /// The threshold for [DatumValue::honor_small].
+    ///
+    /// [DatumValue::honor_small]: value::DatumValue::honor_small
     pub small: f64,
 
     pub weight_format: Format,
@@ -983,38 +1362,66 @@ impl PivotTableStyle {
     }
 }
 
+/// Metadata for a [PivotTable].
 #[derive(Clone, Debug, Default, Serialize)]
 pub struct PivotTableMetadata {
+    /// Title.
+    ///
+    /// The title is displayed above the table.  Every table should have a
+    /// title.
+    pub title: Option<Box<Value>>,
+
+    /// Caption.
+    ///
+    /// The caption is displayed below the table.  Captions are optional.
+    pub caption: Option<Box<Value>>,
+
+    /// Corner text, displayed in the top-left corner of the table.  Corner text
+    /// is optional.
+    pub corner_text: Option<Box<Value>>,
+
+    /// User-specified optional notes, with special variables expanded into
+    /// their values.
+    pub notes: Option<String>,
+
+    /// User-specified optional notes, with special variables left in their
+    /// original forms.
+    ///
+    /// This allows the notes to be edited in their original form and then
+    /// expanded for display.
+    pub notes_unexpanded: Option<String>,
+
+    /// The localized name of the command that produced this pivot table,
+    /// e.g. `Frequencies` translated into the local language.
     pub command_local: Option<String>,
+
+    /// The locale-invariant name of the command that produced this pivot table,
+    /// e.g. `Frequencies`.
     pub command_c: Option<String>,
+
+    /// The locale-invariant command ID for the particular kind of output that
+    /// this table represents in the procedure.  This can be the same as
+    /// `command_c`, e.g. `Frequencies`, or different, e.g. `Case Processing
+    /// Summary`.
+    ///
+    /// `Notes` and `Warnings` are common generic subtypes.
+    pub subtype: Option<Box<Value>>,
+
+    /// The language used in output.
     pub language: Option<String>,
+
+    /// A locale, including an encoding, such as `en_US.windows-1252` or
+    /// `it_IT.windows-1252`.
     pub locale: Option<String>,
+
+    /// Name of the dataset analyzed to produce the output, e.g. `DataSet1`.
     pub dataset: Option<String>,
+
+    /// Name of the file that the dataset is from, e.g. `C:\Users\foo\bar.sav`.
     pub datafile: Option<String>,
+
+    /// Creation date for the table.
     pub date: Option<NaiveDateTime>,
-    pub title: Option<Box<Value>>,
-    pub subtype: Option<Box<Value>>,
-    pub corner_text: Option<Box<Value>>,
-    pub caption: Option<Box<Value>>,
-    pub notes: Option<String>,
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub struct PivotTable {
-    pub style: PivotTableStyle,
-
-    /// Current layer indexes, with `axes[Axis3::Z].dimensions.len()` elements.
-    /// `current_layer[i]` is an offset into
-    /// `axes[Axis3::Z].dimensions[i].data_leaves[]`, except that a dimension
-    /// can have zero leaves, in which case `current_layer[i]` is zero and
-    /// there's no corresponding leaf.
-    pub current_layer: Vec<usize>,
-
-    pub metadata: PivotTableMetadata,
-    pub footnotes: Footnotes,
-    dimensions: Vec<Dimension>,
-    axes: EnumMap<Axis3, Axis>,
-    cells: HashMap<usize, Value>,
 }
 
 impl PivotTableMetadata {
@@ -1026,130 +1433,22 @@ impl PivotTableMetadata {
     }
 }
 
-impl PivotTable {
-    pub fn cells(&self) -> &HashMap<usize, Value> {
-        &self.cells
-    }
-    pub fn dimensions(&self) -> &[Dimension] {
-        &self.dimensions
-    }
-    pub fn axes(&self) -> &EnumMap<Axis3, Axis> {
-        &self.axes
-    }
-
-    pub fn with_title(mut self, title: impl Into<Value>) -> Self {
-        self.metadata.title = Some(Box::new(title.into()));
-        self.style.show_title = true;
-        self
-    }
-
-    pub fn with_caption(mut self, caption: impl Into<Value>) -> Self {
-        self.metadata.caption = Some(Box::new(caption.into()));
-        self.style.show_caption = true;
-        self
-    }
-
-    pub fn with_corner_text(mut self, corner_text: impl Into<Value>) -> Self {
-        self.metadata.corner_text = Some(Box::new(corner_text.into()));
-        self
-    }
-
-    pub fn with_subtype(self, subtype: impl Into<Value>) -> Self {
-        Self {
-            metadata: self.metadata.with_subtype(subtype),
-            ..self
-        }
-    }
-
-    pub fn with_show_values(self, show_values: Option<Show>) -> Self {
-        Self {
-            style: self.style.with_show_values(show_values),
-            ..self
-        }
-    }
-
-    pub fn with_show_variables(self, show_variables: Option<Show>) -> Self {
-        Self {
-            style: self.style.with_show_variables(show_variables),
-            ..self
-        }
-    }
-
-    pub fn with_show_title(self, show_title: bool) -> Self {
-        Self {
-            style: self.style.with_show_title(show_title),
-            ..self
-        }
-    }
-
-    pub fn with_show_caption(self, show_caption: bool) -> Self {
-        Self {
-            style: self.style.with_show_caption(show_caption),
-            ..self
-        }
-    }
-
-    pub fn with_layer(mut self, layer: &[usize]) -> Self {
-        debug_assert_eq!(layer.len(), self.current_layer.len());
-        if self.style.look.print_all_layers {
-            self.style.look_mut().print_all_layers = false;
-        }
-        self.current_layer.clear();
-        self.current_layer.extend_from_slice(layer);
-        self
-    }
-
-    pub fn with_all_layers(mut self) -> Self {
-        if !self.style.look.print_all_layers {
-            self.look_mut().print_all_layers = true;
-        }
-        self
-    }
-
-    pub fn look_mut(&mut self) -> &mut Look {
-        self.style.look_mut()
-    }
-
-    pub fn with_show_empty(mut self) -> Self {
-        if self.style.look.hide_empty {
-            self.look_mut().hide_empty = false;
-        }
-        self
-    }
-
-    pub fn with_hide_empty(mut self) -> Self {
-        if !self.style.look.hide_empty {
-            self.look_mut().hide_empty = true;
-        }
-        self
-    }
-
-    pub fn label(&self) -> String {
-        match &self.metadata.title {
-            Some(title) => title.display(self).to_string(),
-            None => String::from("Table"),
-        }
-    }
+#[derive(Clone, Debug, Serialize)]
+pub struct PivotTable {
+    pub style: PivotTableStyle,
 
-    pub fn title(&self) -> &Value {
-        match &self.metadata.title {
-            Some(title) => title,
-            None => {
-                static EMPTY: Value = Value::empty();
-                &EMPTY
-            }
-        }
-    }
+    /// Current layer indexes, with `axes[Axis3::Z].dimensions.len()` elements.
+    /// `layer[i]` is an offset into
+    /// `axes[Axis3::Z].dimensions[i].data_leaves[]`, except that a dimension
+    /// can have zero leaves, in which case `layer[i]` is zero and there's no
+    /// corresponding leaf.
+    layer: Vec<usize>,
 
-    pub fn subtype(&self) -> &Value {
-        match &self.metadata.subtype {
-            Some(subtype) => subtype,
-            None => {
-                static EMPTY: Value = Value::empty();
-                &EMPTY
-            }
-        }
-    }
+    pub metadata: PivotTableMetadata,
+    pub footnotes: Footnotes,
+    dimensions: Vec<Dimension>,
+    axes: EnumMap<Axis3, Axis>,
+    cells: HashMap<usize, Value>,
 }
 
 impl Default for PivotTable {
@@ -1157,7 +1456,7 @@ impl Default for PivotTable {
         Self {
             style: PivotTableStyle::default(),
             metadata: PivotTableMetadata::default(),
-            current_layer: Vec::new(),
+            layer: Vec::new(),
             footnotes: Footnotes::new(),
             dimensions: Vec::new(),
             axes: EnumMap::default(),
@@ -1201,161 +1500,6 @@ impl CellIndex for PrecomputedIndex {
     }
 }
 
-impl PivotTable {
-    pub fn new(axes_and_dimensions: impl IntoIterator<Item = (Axis3, Dimension)>) -> Self {
-        let mut dimensions = Vec::new();
-        let mut axes = EnumMap::<Axis3, Axis>::default();
-        for (axis, dimension) in axes_and_dimensions {
-            axes[axis].dimensions.push(dimensions.len());
-            dimensions.push(dimension);
-        }
-        Self {
-            style: PivotTableStyle::default().with_look(Settings::global().look.clone()),
-            current_layer: repeat_n(0, axes[Axis3::Z].dimensions.len()).collect(),
-            axes,
-            dimensions,
-            ..Self::default()
-        }
-    }
-    fn cell_index<C>(&self, cell_index: C) -> usize
-    where
-        C: CellIndex,
-    {
-        cell_index.cell_index(self.dimensions.iter().map(|d| d.len()))
-    }
-
-    pub fn insert<C>(&mut self, cell_index: C, value: impl Into<Value>)
-    where
-        C: CellIndex,
-    {
-        self.cells.insert(self.cell_index(cell_index), value.into());
-    }
-
-    pub fn get<C>(&self, cell_index: C) -> Option<&Value>
-    where
-        C: CellIndex,
-    {
-        self.cells.get(&self.cell_index(cell_index))
-    }
-
-    pub fn with_data<C>(mut self, iter: impl IntoIterator<Item = (C, Value)>) -> Self
-    where
-        C: CellIndex,
-    {
-        self.extend(iter);
-        self
-    }
-
-    pub fn with_style(self, style: PivotTableStyle) -> Self {
-        Self { style, ..self }
-    }
-    pub fn with_metadata(self, metadata: PivotTableMetadata) -> Self {
-        Self { metadata, ..self }
-    }
-
-    /// Converts per-axis presentation-order indexes in `presentation_indexes`,
-    /// into data indexes for each dimension.
-    fn convert_indexes_ptod(
-        &self,
-        presentation_indexes: EnumMap<Axis3, &[usize]>,
-    ) -> SmallVec<[usize; 4]> {
-        let mut data_indexes = SmallVec::from_elem(0, self.dimensions.len());
-        for (axis, presentation_indexes) in presentation_indexes {
-            for (&dim_index, &pindex) in self.axes[axis]
-                .dimensions
-                .iter()
-                .zip(presentation_indexes.iter())
-            {
-                data_indexes[dim_index] = self.dimensions[dim_index].presentation_order[pindex];
-            }
-        }
-        data_indexes
-    }
-
-    /// Returns an iterator for the layer axis:
-    ///
-    /// - If `print` is true and `self.look.print_all_layers`, then the iterator
-    ///   will visit all values of the layer axis.
-    ///
-    /// - Otherwise, the iterator will just visit `self.current_layer`.
-    pub fn layers(&self, print: bool) -> Box<dyn Iterator<Item = SmallVec<[usize; 4]>>> {
-        if print && self.style.look.print_all_layers {
-            Box::new(self.axis_values(Axis3::Z))
-        } else {
-            Box::new(once(SmallVec::from_slice(&self.current_layer)))
-        }
-    }
-
-    pub fn value_options(&self) -> ValueOptions {
-        ValueOptions {
-            show_values: self.style.show_values,
-            show_variables: self.style.show_variables,
-            small: self.style.small,
-            footnote_marker_type: self.style.look.footnote_marker_type,
-        }
-    }
-
-    pub fn transpose(&mut self) {
-        self.axes.swap(Axis3::X, Axis3::Y);
-    }
-
-    pub fn axis_dimensions(
-        &self,
-        axis: Axis3,
-    ) -> impl DoubleEndedIterator<Item = &Dimension> + ExactSizeIterator {
-        self.axes[axis]
-            .dimensions
-            .iter()
-            .copied()
-            .map(|index| &self.dimensions[index])
-    }
-
-    fn find_dimension(&self, dim_index: usize) -> Option<(Axis3, usize)> {
-        debug_assert!(dim_index < self.dimensions.len());
-        for axis in enum_iterator::all::<Axis3>() {
-            for (position, dimension) in self.axes[axis].dimensions.iter().copied().enumerate() {
-                if dimension == dim_index {
-                    return Some((axis, position));
-                }
-            }
-        }
-        None
-    }
-    pub fn move_dimension(&mut self, dim_index: usize, new_axis: Axis3, new_position: usize) {
-        let (old_axis, old_position) = self.find_dimension(dim_index).unwrap();
-        if old_axis == new_axis && old_position == new_position {
-            return;
-        }
-
-        // Update the current layer, if necessary.  If we're moving within the
-        // layer axis, preserve the current layer.
-        match (old_axis, new_axis) {
-            (Axis3::Z, Axis3::Z) => {
-                // Rearrange the layer axis.
-                if old_position < new_position {
-                    self.current_layer[old_position..=new_position].rotate_left(1);
-                } else {
-                    self.current_layer[new_position..=old_position].rotate_right(1);
-                }
-            }
-            (Axis3::Z, _) => {
-                // A layer is becoming a row or column.
-                self.current_layer.remove(old_position);
-            }
-            (_, Axis3::Z) => {
-                // A row or column is becoming a layer.
-                self.current_layer.insert(new_position, 0);
-            }
-            _ => (),
-        }
-
-        self.axes[old_axis].dimensions.remove(old_position);
-        self.axes[new_axis]
-            .dimensions
-            .insert(new_position, dim_index);
-    }
-}
-
 impl<C> Extend<(C, Value)> for PivotTable
 where
     C: CellIndex,
index 58c716138c9f0e34cd65f737e2ae28d13cf59f3e..7157377d763c39fed3f85f5bdcad1bca25f9477f 100644 (file)
@@ -24,6 +24,7 @@
 
 // Warn about missing docs, but not for items declared with `#[cfg(test)]`.
 #![cfg_attr(not(test), warn(missing_docs))]
+#![warn(dead_code)]
 
 use std::{
     fmt::{Debug, Display},
index f71f2c733fbb59065763c0e675d33167af853ac3..c5a0ebfdfc40b736bfc4466d11102da74b0f79b9 100644 (file)
 
 // Warn about missing docs, but not for items declared with `#[cfg(test)]`.
 #![cfg_attr(not(test), warn(missing_docs))]
+#![warn(dead_code)]
 
 use crate::{
     calendar::{date_time_to_pspp, time_to_pspp},
     data::{ByteString, Datum, EncodedString, WithEncoding},
     format::{DATETIME40_0, F8_2, F40, Format, TIME40_0, Type, UncheckedFormat},
     output::pivot::{
-        DisplayMarker, Footnote, FootnoteMarkerType, PivotTable,
+        DisplayMarker, Footnote, FootnoteMarkerType,
         look::{CellStyle, FontStyle},
     },
     settings::{Settings, Show},
@@ -108,18 +109,86 @@ impl Value {
         }
     }
 
-    /// Constructs a new `Value` as a number whose value is `date_time`
-    /// converted to the [PSPP date representation](crate::calendar).
+    /// Constructs a new `Value` from `number` with a default [F8_2] format.
+    /// Some related useful methods are:
+    ///
+    /// - [with_source_variable], to add information about the variable that the
+    ///   datum came from (or use [new_datum_from_variable] as a shortcut to
+    ///   combine both).
+    ///
+    /// - [with_format] to override the default format.
+    ///
+    /// [with_source_variable]: Self::with_source_variable
+    /// [new_datum_from_variable]: Self::new_datum_from_variable
+    /// [with_format]: Self::with_format
+    pub fn new_number(number: Option<f64>) -> Self {
+        Self::new(ValueInner::Datum(DatumValue::new_number(number)))
+    }
+
+    /// Construct a new `Value` from `number` with format [F8_0].
+    ///
+    /// [F8_0]: crate::format::F8_0
+    pub fn new_integer(x: Option<f64>) -> Self {
+        Self::new_number(x).with_format(F40)
+    }
+
+    /// Constructs a new `Value` as a number whose value is `date_time`, which
+    /// is converted to the [PSPP date representation](crate::calendar), with
+    /// format [DATETIME40_0].
     pub fn new_date(date_time: NaiveDateTime) -> Self {
         Self::new_number(Some(date_time_to_pspp(date_time))).with_format(DATETIME40_0)
     }
 
-    /// Constructs a new `Value` as a number whose value is `time`
-    /// converted to the [PSPP time representation](crate::calendar).
+    /// Constructs a new `Value` as a number whose value is `time`, which is
+    /// converted to the [PSPP time representation](crate::calendar), with
+    /// format [TIME40_0].
     pub fn new_time(time: NaiveTime) -> Self {
         Self::new_number(Some(time_to_pspp(time))).with_format(TIME40_0)
     }
 
+    /// Constructs a new `Value` from localizable text string `s`.
+    ///
+    /// PSPP doesn't support internationalization yet, so this does the same
+    /// thing as [new_user_text] for now.
+    ///
+    /// [new_user_text]: Self::new_user_text
+    pub fn new_text(s: impl Into<String>) -> Self {
+        Self::new_user_text(s)
+    }
+
+    /// Constructs a new `Value` from localizable text string `localized`,
+    /// English string `c`, and identifier `id`.  If the string came from the
+    /// user, `user_provided` should be true.
+    pub fn new_general_text(localized: String, c: String, id: String, user_provided: bool) -> Self {
+        Self::new(ValueInner::Text(TextValue {
+            user_provided,
+            c: (c != localized).then_some(c),
+            id: (id != localized).then_some(id),
+            localized,
+        }))
+    }
+
+    /// Constructs a new `Value` from `markup`.
+    pub fn new_markup(markup: Markup) -> Self {
+        Self::new(ValueInner::Markup(markup))
+    }
+
+    /// 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,
+            }))
+        }
+    }
+
     /// Constructs a new `Value` from `variable`.
     pub fn new_variable(variable: &Variable) -> Self {
         Self::new(ValueInner::Variable(VariableValue {
@@ -129,11 +198,11 @@ impl Value {
         }))
     }
 
-    /// Construct a new `Value` from `datum` with a default format.  Some
+    /// Constructs a new `Value` from `datum` with a default format.  Some
     /// related useful methods are:
     ///
     /// - [with_source_variable], to add information about the variable that the
-    ///   datum came from (or use [new_datum_from_variable] as a convenience to
+    ///   datum came from (or use [new_datum_from_variable] as a shortcut to
     ///   combine both).
     ///
     /// - [with_format] to override the default format.
@@ -148,21 +217,28 @@ impl Value {
         Self::new(ValueInner::Datum(DatumValue::new(datum)))
     }
 
-    /// Returns this value with its display format set to `format`.
-    pub fn with_format(self, format: Format) -> Self {
-        Self {
-            inner: self.inner.with_format(format),
-            ..self
+    /// Construct a new, empty `Value`.
+    pub const fn new_empty() -> Self {
+        // Can't use `Self::default()` because that is non-const.
+        Value {
+            inner: ValueInner::Empty,
+            styling: None,
         }
     }
 
-    pub fn with_honor_small(self, honor_small: bool) -> Self {
-        Self {
-            inner: self.inner.with_honor_small(honor_small),
-            ..self
-        }
+    /// Returns a reference to a statically allocated empty `Value`.
+    pub const fn static_empty() -> &'static Self {
+        static EMPTY: Value = Value::new_empty();
+        &EMPTY
+    }
+
+    /// Returns true if this `Value` is empty and unstyled.
+    pub const fn is_empty(&self) -> bool {
+        self.inner.is_empty() && self.styling.is_none()
     }
 
+    /// Returns this value with its value label, format, and variable name from
+    /// `variable`.
     pub fn with_source_variable(self, variable: &Variable) -> Self {
         let value_label = self
             .datum()
@@ -172,135 +248,158 @@ impl Value {
             .with_variable_name(Some(variable.name.as_str().into()))
     }
 
+    /// Returns this value with its display format set to `format`, if it is a
+    /// [DatumValue].
+    pub fn with_format(self, format: Format) -> Self {
+        Self {
+            inner: self.inner.with_format(format),
+            ..self
+        }
+    }
+
+    /// Returns this value with `honor_small` set as specified, if it is a
+    /// [DatumValue].
+    pub fn with_honor_small(self, honor_small: bool) -> Self {
+        Self {
+            inner: self.inner.with_honor_small(honor_small),
+            ..self
+        }
+    }
+
     /// Construct a new `Value` from `datum`, which is a value of `variable`.
     pub fn new_datum_from_variable(datum: &Datum<ByteString>, variable: &Variable) -> Self {
         Self::new_datum(&datum.as_encoded(variable.encoding())).with_source_variable(variable)
     }
 
+    /// Returns the inner [Datum], if this value is a [DatumValue].
     pub fn datum(&self) -> Option<&Datum<WithEncoding<ByteString>>> {
         self.inner.datum()
     }
 
-    pub fn new_number(number: Option<f64>) -> Self {
-        Self::new(ValueInner::Datum(DatumValue::new_number(number)))
-    }
-
-    pub fn new_integer(x: Option<f64>) -> Self {
-        Self::new_number(x).with_format(F40)
-    }
-    pub fn new_text(s: impl Into<String>) -> Self {
-        Self::new_user_text(s)
-    }
-    pub fn new_general_text(localized: String, c: String, id: String, user_provided: bool) -> Self {
-        Self::new(ValueInner::Text(TextValue {
-            user_provided,
-            c: (c != localized).then_some(c),
-            id: (id != localized).then_some(id),
-            localized,
-        }))
-    }
-    pub fn new_markup(markup: Markup) -> Self {
-        Self::new(ValueInner::Markup(markup))
-    }
-    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,
-            }))
-        }
-    }
+    /// Returns this `Value` with the added `footnote`.
     pub fn with_footnote(mut self, footnote: &Arc<Footnote>) -> Self {
         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;
         footnotes.push(footnote.clone());
         footnotes.sort_by_key(|f| f.index);
     }
+
+    /// 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 {
         if let Some(datum_value) = self.inner.as_datum_value_mut() {
             datum_value.show = show;
         }
         self
     }
-    pub fn with_show_variable_label(mut self, show: Option<Show>) -> Self {
-        if let ValueInner::Variable(variable_value) = &mut self.inner {
-            variable_value.show = show;
-        }
-        self
-    }
+
+    /// Returns this `Value` with `value_label` as the value label, if this is a
+    /// [DatumValue].
+    ///
+    /// Use [with_source_variable], instead, to automatically add a value label
+    /// and other information from a source variable.
+    ///
+    /// [with_source_variable]: Self::with_source_variable
     pub fn with_value_label(mut self, value_label: Option<String>) -> Self {
         if let Some(datum_value) = self.inner.as_datum_value_mut() {
             datum_value.value_label = value_label.clone()
         }
         self
     }
+
+    /// Returns this `Value` with `variable_name` as the variable's name, if
+    /// this is a [DatumValue].
+    ///
+    /// Use [with_source_variable], instead, to automatically add a variable
+    /// name and other information from a source variable.
+    ///
+    /// [with_source_variable]: Self::with_source_variable
     pub fn with_variable_name(mut self, variable_name: Option<String>) -> Self {
-        match &mut self.inner {
-            ValueInner::Datum(DatumValue { variable, .. }) => *variable = variable_name,
-            ValueInner::Variable(VariableValue {
-                var_name: variable, ..
-            }) => {
-                if let Some(name) = variable_name {
-                    *variable = name;
-                }
-            }
-            _ => (),
+        if let Some(datum_value) = self.inner.as_datum_value_mut() {
+            datum_value.variable = variable_name.clone()
         }
         self
     }
-    pub fn styling_mut(&mut self) -> &mut ValueStyle {
-        self.styling.get_or_insert_default()
+
+    /// Returns this `Value` with `show` as the [Show] setting for variable
+    /// labels, if this is a [VariableValue].
+    pub fn with_show_variable_label(mut self, show: Option<Show>) -> Self {
+        if let ValueInner::Variable(variable_value) = &mut self.inner {
+            variable_value.show = show;
+        }
+        self
     }
+
+    /// Returns this `Value` with the specified `font_style`.
     pub fn with_font_style(mut self, font_style: FontStyle) -> Self {
         self.styling_mut().font_style = Some(font_style);
         self
     }
+
+    /// Returns this `Value` with the specified `cell_style`.
     pub fn with_cell_style(mut self, cell_style: CellStyle) -> Self {
         self.styling_mut().cell_style = Some(cell_style);
         self
     }
+
+    /// Returns this `Value` with the specified `styling`.
     pub fn with_styling(self, styling: Option<Box<ValueStyle>>) -> Self {
         Self { styling, ..self }
     }
+
+    /// Returns the styling for this `Value` for modification.
+    ///
+    /// If this `Value` doesn't have styling yet, this creates it.
+    pub fn styling_mut(&mut self) -> &mut ValueStyle {
+        self.styling.get_or_insert_default()
+    }
+
+    /// Returns this `Value`'s font style, if it has one.
     pub fn font_style(&self) -> Option<&FontStyle> {
         self.styling
             .as_ref()
             .map(|styling| styling.font_style.as_ref())
             .flatten()
     }
+
+    /// Returns this `Value`'s cell style, if it has one.
     pub fn cell_style(&self) -> Option<&CellStyle> {
         self.styling
             .as_ref()
             .map(|styling| styling.cell_style.as_ref())
             .flatten()
     }
+
+    /// Returns this `Value`'s subscripts.
     pub fn subscripts(&self) -> &[String] {
         self.styling
             .as_ref()
             .map_or(&[], |styling| &styling.subscripts)
     }
+
+    /// Returns this `Value`'s footnotes.
     pub fn footnotes(&self) -> &[Arc<Footnote>] {
         self.styling
             .as_ref()
             .map_or(&[], |styling| &styling.footnotes)
     }
-    pub const fn empty() -> Self {
-        Value {
-            inner: ValueInner::Empty,
-            styling: None,
+
+    /// Returns an object that will format this value, including subscripts and
+    /// superscripts and footnotes.  `options` controls whether variable and
+    /// value labels are included.
+    pub fn display(&self, options: impl IntoValueOptions) -> DisplayValue<'_> {
+        let display = self.inner.display(options.into_value_options());
+        match &self.styling {
+            Some(styling) => display.with_styling(styling),
+            None => display,
         }
     }
-    pub const fn is_empty(&self) -> bool {
-        self.inner.is_empty() && self.styling.is_none()
-    }
+
     /// Serializes this value in a plain way, like [BareValue].  This function
     /// can be used on a field as `#[serde(serialize_with =
     /// Value::serialize_bare)]`.
@@ -330,6 +429,10 @@ impl From<&Variable> for Value {
     }
 }
 
+/// Helper struct for printing a [Value] with `format!` and `{}`.
+///
+/// Create this struct with [Value::display].
+#[derive(Clone, Debug)]
 pub struct DisplayValue<'a> {
     inner: &'a ValueInner,
     subscripts: &'a [String],
@@ -340,25 +443,32 @@ pub struct DisplayValue<'a> {
 }
 
 impl<'a> DisplayValue<'a> {
-    pub fn subscripts(&self) -> impl Iterator<Item = &str> {
+    /// Returns the subscripts to be displayed, as an iterator of `&str`.
+    pub fn subscripts(&self) -> impl Iterator<Item = &str> + ExactSizeIterator + Clone {
         self.subscripts.iter().map(String::as_str)
     }
 
+    /// Returns true if the value to be displayed includes subscripts.
     pub fn has_subscripts(&self) -> bool {
         !self.subscripts.is_empty()
     }
 
-    pub fn footnotes(&self) -> impl Iterator<Item = DisplayMarker<'_>> {
+    /// Returns the footnotes to be displayed, as an iterator of
+    /// [DisplayMarker].
+    pub fn footnotes(&self) -> impl Iterator<Item = DisplayMarker<'_>> + Clone {
         self.footnotes
             .iter()
             .filter(|f| f.show)
             .map(|f| f.display_marker(self.options))
     }
 
+    /// Returns true if the value to be displayed includes footnotes.
     pub fn has_footnotes(&self) -> bool {
         self.footnotes().next().is_some()
     }
 
+    /// Returns this [DisplayValue] modified so that it won't show any
+    /// subscripts or footnotes.
     pub fn without_suffixes(self) -> Self {
         Self {
             subscripts: &[],
@@ -367,34 +477,47 @@ impl<'a> DisplayValue<'a> {
         }
     }
 
+    /// Returns this [DisplayValue] modified so that it will only show the
+    /// suffixes and footnotes, not the body.
+    pub fn without_body(self) -> Self {
+        Self {
+            inner: &ValueInner::Empty,
+            ..self
+        }
+    }
+
+    /// Returns the [Markup] to be formatted, if any.
     pub fn markup(&self) -> Option<&Markup> {
-        self.inner.markup()
+        self.inner.as_markup()
     }
 
     /// 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) {
-        let suffixes = Self {
-            inner: &ValueInner::Empty,
-            ..self
-        };
-        (self.without_suffixes(), suffixes)
+        (self.clone().without_suffixes(), self.without_body())
     }
 
+    /// Returns this display with subscripts and footnotes taken from `styling`.
+    ///
+    /// (This display can't use the other parts of `styling`, since we're just
+    /// formatting plain text.)
     pub fn with_styling(mut self, styling: &'a ValueStyle) -> Self {
         self.subscripts = styling.subscripts.as_slice();
         self.footnotes = styling.footnotes.as_slice();
         self
     }
 
+    /// Returns this display with the given `subscripts.`
     pub fn with_subscripts(self, subscripts: &'a [String]) -> Self {
         Self { subscripts, ..self }
     }
 
+    /// Returns this display with the given `footnotes.`
     pub fn with_footnotes(self, footnotes: &'a [Arc<Footnote>]) -> Self {
         Self { footnotes, ..self }
     }
 
+    /// Returns true if this display will format to the empty string.
     pub fn is_empty(&self) -> bool {
         self.inner.is_empty() && self.subscripts.is_empty() && self.footnotes.is_empty()
     }
@@ -403,6 +526,17 @@ impl<'a> DisplayValue<'a> {
         self.options.small
     }
 
+    /// Returns a variable type for the value to be displayed.
+    ///
+    /// We consider a numeric value displayed by itself to be numeric, but if
+    /// the value label is displayed then it is considered to be a string.
+    /// Anything else is also a string.
+    ///
+    /// This is useful for passing to [HorzAlign::for_mixed], although maybe
+    /// this method should just return [HorzAlign] directly.
+    ///
+    /// [HorzAlign]: crate::output::pivot::look::HorzAlign
+    /// [HorzAlign::for_mixed]: crate::output::pivot::look::HorzAlign::for_mixed
     pub fn var_type(&self) -> VarType {
         if let Some(datum_value) = self.inner.as_datum_value()
             && datum_value.datum.is_number()
@@ -438,19 +572,6 @@ impl Display for DisplayValue<'_> {
     }
 }
 
-impl Value {
-    // Returns an object that will format this value, including subscripts and
-    // superscripts and footnotes.  `options` controls whether variable and
-    // value labels are included.
-    pub fn display(&self, options: impl IntoValueOptions) -> DisplayValue<'_> {
-        let display = self.inner.display(options.into_value_options());
-        match &self.styling {
-            Some(styling) => display.with_styling(styling),
-            None => display,
-        }
-    }
-}
-
 impl Debug for Value {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let name = match &self.inner {
@@ -462,7 +583,7 @@ impl Debug for Value {
             ValueInner::Empty => "Empty",
         };
         write!(f, "{name}:{:?}", self.display(()).to_string())?;
-        if let Some(markup) = self.inner.markup() {
+        if let Some(markup) = self.inner.as_markup() {
             write!(f, " (markup: {markup:?})")?;
         }
         if let Some(styling) = &self.styling {
@@ -486,9 +607,11 @@ pub struct DatumValue {
     /// If this is unset, then a higher-level default is used.
     pub show: Option<Show>,
 
-    /// If true, then numbers smaller than a threshold will be displayed in
-    /// scientific notation.  Otherwise, all numbers will be displayed with
-    /// `format`.
+    /// If true, then numbers smaller than [PivotTableStyle::small] will be
+    /// displayed in scientific notation.  Otherwise, all numbers will be
+    /// displayed with `format`.
+    ///
+    /// [PivotTableStyle::small]: super::PivotTableStyle::small
     pub honor_small: bool,
 
     /// The name of the variable that `value` came from, if any.
@@ -527,9 +650,7 @@ impl Serialize for DatumValue {
 }
 
 impl DatumValue {
-    pub fn new_number(number: Option<f64>) -> Self {
-        Self::new(&Datum::<&str>::Number(number))
-    }
+    /// Constructs a new `DatumValue` for `datum`.
     pub fn new<B>(datum: &Datum<B>) -> Self
     where
         B: EncodedString,
@@ -543,15 +664,26 @@ impl DatumValue {
             value_label: None,
         }
     }
+
+    /// Constructs a new `DatumValue` for `number`.
+    pub fn new_number(number: Option<f64>) -> Self {
+        Self::new(&Datum::<&str>::Number(number))
+    }
+
+    /// Returns this `DatumValue` with the given `format`.
     pub fn with_format(self, format: Format) -> Self {
         Self { format, ..self }
     }
+
+    /// Returns this `DatumValue` with the given `honor_small`.
     pub fn with_honor_small(self, honor_small: bool) -> Self {
         Self {
             honor_small,
             ..self
         }
     }
+
+    /// Writes this value to `f` using the settings in `display`.
     pub fn display<'a>(
         &self,
         display: &DisplayValue<'a>,
@@ -590,6 +722,8 @@ impl DatumValue {
         Ok(())
     }
 
+    /// Serializes this value to `serializer` in the "bare" manner described for
+    /// [BareValue].
     pub fn serialize_bare<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
@@ -639,9 +773,9 @@ impl VariableValue {
 
 /// A text string.
 ///
-/// Whereas a [StringValue] is usually related to data, a `TextValue` is used
-/// for other text within a table, such as a title, a column or row heading, or
-/// a footnote.
+/// A `TextValue` is used for text within a table, such as a title, a column or
+/// row heading, or a footnote.  (String data values are better represented as
+/// [DatumValue].)
 #[derive(Clone, Debug, PartialEq)]
 pub struct TextValue {
     /// Whether the text came from the user.
@@ -700,12 +834,17 @@ impl Display for TextValue {
 }
 
 impl TextValue {
+    /// Returns the localized version of this `TextValue`.
     pub fn localized(&self) -> &str {
         self.localized.as_str()
     }
+
+    /// Returns the English version of this `TextValue`.
     pub fn c(&self) -> &str {
         self.c.as_ref().unwrap_or(&self.localized).as_str()
     }
+
+    /// Returns an identifier for this `TextValue`.
     pub fn id(&self) -> &str {
         self.id.as_ref().unwrap_or(&self.localized).as_str()
     }
@@ -763,7 +902,7 @@ impl TemplateValue {
                         && let Some(arg) = self.args.get(index)
                         && let Some(arg) = arg.first()
                     {
-                        arg.display(display.options).fmt(f)?;
+                        write!(f, "{}", arg.display(display.options))?;
                     }
                     iter = rest.chars();
                 }
@@ -877,9 +1016,13 @@ pub enum ValueInner {
 }
 
 impl ValueInner {
+    /// Returns true if this is a [ValueInner::Empty].
     pub const fn is_empty(&self) -> bool {
         matches!(self, Self::Empty)
     }
+
+    /// Returns this value with its display format set to `format`, if it is a
+    /// [DatumValue].
     pub fn with_format(mut self, format: Format) -> Self {
         if let Some(datum_value) = self.as_datum_value_mut() {
             datum_value.format = format;
@@ -887,6 +1030,8 @@ impl ValueInner {
         self
     }
 
+    /// Returns this value with `honor_small` set as specified, if it is a
+    /// [DatumValue].
     pub fn with_honor_small(mut self, honor_small: bool) -> Self {
         if let Some(datum_value) = self.as_datum_value_mut() {
             datum_value.honor_small = honor_small;
@@ -894,10 +1039,13 @@ impl ValueInner {
         self
     }
 
+    /// Returns the [Datum] inside this value, if it is a [DatumValue].
     pub fn datum(&self) -> Option<&Datum<WithEncoding<ByteString>>> {
         self.as_datum_value().map(|d| &d.datum)
     }
 
+    /// Returns the [Show] value inside this value, if it has one, or [None]
+    /// otherwise.
     fn show(&self) -> Option<Show> {
         match self {
             ValueInner::Datum(DatumValue { show, .. })
@@ -906,47 +1054,70 @@ impl ValueInner {
         }
     }
 
-    fn label(&self) -> Option<&str> {
+    /// Returns the value label or variable label inside this value, if it has
+    /// one.
+    pub fn label(&self) -> Option<&str> {
         self.value_label().or_else(|| self.variable_label())
     }
 
+    /// Returns the value label inside this value, if it has one.
     fn value_label(&self) -> Option<&str> {
         self.as_datum_value()
             .and_then(|d| d.value_label.as_ref().map(String::as_str))
     }
 
+    /// Returns the variable label inside this value, if it has one.
     fn variable_label(&self) -> Option<&str> {
+        self.as_variable_value()
+            .and_then(|d| d.variable_label.as_ref().map(String::as_str))
+    }
+
+    /// Returns the [DatumValue] inside this value, if it is
+    /// [ValueInner::Datum].
+    pub fn as_datum_value(&self) -> Option<&DatumValue> {
         match self {
-            ValueInner::Variable(VariableValue { variable_label, .. }) => {
-                variable_label.as_ref().map(String::as_str)
-            }
+            ValueInner::Datum(datum) => Some(datum),
             _ => None,
         }
     }
 
-    fn markup(&self) -> Option<&Markup> {
+    /// Returns the [DatumValue] inside this value, mutably, if it is
+    /// [ValueInner::Datum].
+    pub fn as_datum_value_mut(&mut self) -> Option<&mut DatumValue> {
         match self {
-            ValueInner::Markup(markup) => Some(markup),
+            ValueInner::Datum(datum) => Some(datum),
             _ => None,
         }
     }
 
-    pub fn as_datum_value(&self) -> Option<&DatumValue> {
+    /// Returns the [VariableValue] inside this value, if it is
+    /// [ValueInner::Variable].
+    pub fn as_variable_value(&self) -> Option<&VariableValue> {
         match self {
-            ValueInner::Datum(datum) => Some(datum),
+            ValueInner::Variable(variable) => Some(variable),
             _ => None,
         }
     }
 
-    pub fn as_datum_value_mut(&mut self) -> Option<&mut DatumValue> {
+    /// Returns the [VariableValue] inside this value, mutably, if it is
+    /// [ValueInner::Variable].
+    pub fn as_variable_value_mut(&mut self) -> Option<&mut VariableValue> {
         match self {
-            ValueInner::Datum(datum) => Some(datum),
+            ValueInner::Variable(variable) => Some(variable),
             _ => None,
         }
     }
 
-    // Returns an object that will format this value.  Settings on `options`
-    // control whether variable and value labels are included.
+    /// Returns the [Markup] inside this value, if it is [ValueInner::Markup].
+    fn as_markup(&self) -> Option<&Markup> {
+        match self {
+            ValueInner::Markup(markup) => Some(markup),
+            _ => None,
+        }
+    }
+
+    /// Returns an object that will format this value.  Settings on `options`
+    /// control whether variable and value labels are included.
     pub fn display(&self, options: impl IntoValueOptions) -> DisplayValue<'_> {
         fn interpret_show(
             global_show: impl Fn() -> Show,
@@ -990,15 +1161,30 @@ impl ValueInner {
     }
 }
 
+/// Styling inside a [Value].
+///
+/// Most [Value]s use a default style, so this is a separate [Box]ed structure
+/// to save memory.
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct ValueStyle {
+    /// Cell style.
     pub cell_style: Option<CellStyle>,
+
+    /// Font style.
     pub font_style: Option<FontStyle>,
+
+    /// Subscripts.
     pub subscripts: Vec<String>,
+
+    /// Footnotes.
     pub footnotes: Vec<Arc<Footnote>>,
 }
 
 impl ValueStyle {
+    /// Returns true if this [ValueStyle] is empty.
+    ///
+    /// This will return false if the font style exists but is the default font
+    /// style, and similarly for the cell style.
     pub fn is_empty(&self) -> bool {
         self.font_style.is_none()
             && self.cell_style.is_none()
@@ -1052,13 +1238,6 @@ impl IntoValueOptions for () {
     }
 }
 
-/// Extracts [ValueOptions] from a pivot table.
-impl IntoValueOptions for &PivotTable {
-    fn into_value_options(self) -> ValueOptions {
-        self.value_options()
-    }
-}
-
 /// Copies [ValueOptions] by reference.
 impl IntoValueOptions for &ValueOptions {
     fn into_value_options(self) -> ValueOptions {
index 3162508801278455b042df3579570dce6201eaf5..6fb039923f5d63771890bf0dc6a0a84205fca924 100644 (file)
@@ -997,7 +997,7 @@ impl Pager {
         layer_indexes: Option<&[usize]>,
     ) -> Self {
         let output = pivot_table.output(
-            layer_indexes.unwrap_or(&pivot_table.current_layer),
+            layer_indexes.unwrap_or(pivot_table.layer()),
             device.params().printing,
         );
 
index b3bef464a66fa3f6b0f1801a5f50bc178789a6a9..04fbea4d69b9f2537a41def101da0728d09dd9c1 100644 (file)
@@ -504,7 +504,7 @@ impl Visualization {
                     variables[0]
                         .label
                         .as_ref()
-                        .map_or_else(|| Value::empty(), |label| Value::new_user_text(label)),
+                        .map_or_else(|| Value::new_empty(), |label| Value::new_user_text(label)),
                 )
                 .with_multiple(cats.into_iter().map(|cb| cb.category))
                 .with_show_label(show_label),
index 21f549f9db81a6943b18bd2635a6e4f2e9241b71..c557be16259b1f8435c72f75369a3560454b6a38 100644 (file)
@@ -217,6 +217,8 @@ impl LightTable {
                     .as_ref()
                     .map(|caption| Box::new(caption.decode(encoding, &footnotes))),
                 notes: self.table_settings.notes.decode_optional(encoding),
+                notes_unexpanded: n3_inner
+                    .and_then(|inner| inner.notes_unexpanded.decode_optional(encoding)),
             })
             .with_footnotes(footnotes)
             .with_data(cells);
index 042db878856d56324a36758ca25c3d2b4dc9cf85..b275f7ce2b58e366eb3752ce829520254347e61e 100644 (file)
@@ -537,7 +537,7 @@ impl PivotTable {
         let mut layer = 0;
         for (dimension, layer_value) in self
             .axis_dimensions(Axis3::Z)
-            .zip(self.current_layer.iter().copied())
+            .zip(self.layer().iter().copied())
             .rev()
         {
             layer = layer * dimension.len() + layer_value;