From 8739d8179d5f75e368605089be2bd8942f14c2be Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Sun, 14 Dec 2025 11:44:49 -0800 Subject: [PATCH] work --- rust/pspp/src/dictionary.rs | 2 +- rust/pspp/src/output/pivot.rs | 956 +++++++++++++++------------ rust/pspp/src/output/pivot/look.rs | 1 + rust/pspp/src/output/pivot/value.rs | 433 ++++++++---- rust/pspp/src/output/render.rs | 2 +- rust/pspp/src/spv/read/legacy_xml.rs | 2 +- rust/pspp/src/spv/read/light.rs | 2 + rust/pspp/src/spv/write.rs | 2 +- 8 files changed, 863 insertions(+), 537 deletions(-) diff --git a/rust/pspp/src/dictionary.rs b/rust/pspp/src/dictionary.rs index 73ce62700e..c65e695b35 100644 --- a/rust/pspp/src/dictionary.rs +++ b/rust/pspp/src/dictionary.rs @@ -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"); diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 152e62c489..f72874ed33 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -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, } -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) -> Self { + let mut dims = Vec::new(); + let mut axes = EnumMap::::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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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, 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, - - /// 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 { + &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> { - 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 { - 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 { + &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(&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(&mut self, cell_index: C, value: impl Into) + where + C: CellIndex, + { + self.cells.insert(self.cell_index(cell_index), value.into()); } - pub fn as_leaf(&self) -> Option { - (self.level == 0).then_some(self.leaf_index) + /// Returns the cell with the given `cell_index`, if there is one. + pub fn get(&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, + /// Returns the pivot table with cell indexes and values from `iter` + /// inserted as data. + pub fn with_data(mut self, iter: impl IntoIterator) -> 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, + ) -> 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>> { + 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 + 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::() { + 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, + + /// 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> { + self.root.leaf_path(index, SmallVec::new()) + } + + pub fn index_path(&self, index: usize) -> Option { + 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 { + (self.level == 0).then_some(self.leaf_index) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct Group { + #[serde(skip)] + len: usize, + pub name: Box, + + /// The child categories. + /// /// A group usually has multiple children, but it is allowed to have /// only one or even (pathologically) none. pub children: Vec, @@ -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>); @@ -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, + /// 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, + /// 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, + /// 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, + /// 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>, + + /// Caption. + /// + /// The caption is displayed below the table. Captions are optional. + pub caption: Option>, + + /// Corner text, displayed in the top-left corner of the table. Corner text + /// is optional. + pub corner_text: Option>, + + /// User-specified optional notes, with special variables expanded into + /// their values. + pub notes: Option, + + /// 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, + + /// The localized name of the command that produced this pivot table, + /// e.g. `Frequencies` translated into the local language. pub command_local: Option, + + /// The locale-invariant name of the command that produced this pivot table, + /// e.g. `Frequencies`. pub command_c: Option, + + /// 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>, + + /// The language used in output. pub language: Option, + + /// A locale, including an encoding, such as `en_US.windows-1252` or + /// `it_IT.windows-1252`. pub locale: Option, + + /// Name of the dataset analyzed to produce the output, e.g. `DataSet1`. pub dataset: Option, + + /// Name of the file that the dataset is from, e.g. `C:\Users\foo\bar.sav`. pub datafile: Option, + + /// Creation date for the table. pub date: Option, - pub title: Option>, - pub subtype: Option>, - pub corner_text: Option>, - pub caption: Option>, - pub notes: Option, -} - -#[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, - - pub metadata: PivotTableMetadata, - pub footnotes: Footnotes, - dimensions: Vec, - axes: EnumMap, - cells: HashMap, } impl PivotTableMetadata { @@ -1026,130 +1433,22 @@ impl PivotTableMetadata { } } -impl PivotTable { - pub fn cells(&self) -> &HashMap { - &self.cells - } - pub fn dimensions(&self) -> &[Dimension] { - &self.dimensions - } - pub fn axes(&self) -> &EnumMap { - &self.axes - } - - pub fn with_title(mut self, title: impl Into) -> Self { - self.metadata.title = Some(Box::new(title.into())); - self.style.show_title = true; - self - } - - pub fn with_caption(mut self, caption: impl Into) -> 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) -> Self { - self.metadata.corner_text = Some(Box::new(corner_text.into())); - self - } - - pub fn with_subtype(self, subtype: impl Into) -> Self { - Self { - metadata: self.metadata.with_subtype(subtype), - ..self - } - } - - pub fn with_show_values(self, show_values: Option) -> Self { - Self { - style: self.style.with_show_values(show_values), - ..self - } - } - - pub fn with_show_variables(self, show_variables: Option) -> 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, - 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, + axes: EnumMap, + cells: HashMap, } 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) -> Self { - let mut dimensions = Vec::new(); - let mut axes = EnumMap::::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(&self, cell_index: C) -> usize - where - C: CellIndex, - { - cell_index.cell_index(self.dimensions.iter().map(|d| d.len())) - } - - pub fn insert(&mut self, cell_index: C, value: impl Into) - where - C: CellIndex, - { - self.cells.insert(self.cell_index(cell_index), value.into()); - } - - pub fn get(&self, cell_index: C) -> Option<&Value> - where - C: CellIndex, - { - self.cells.get(&self.cell_index(cell_index)) - } - - pub fn with_data(mut self, iter: impl IntoIterator) -> 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, - ) -> 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>> { - 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 + 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::() { - 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 Extend<(C, Value)> for PivotTable where C: CellIndex, diff --git a/rust/pspp/src/output/pivot/look.rs b/rust/pspp/src/output/pivot/look.rs index 58c716138c..7157377d76 100644 --- a/rust/pspp/src/output/pivot/look.rs +++ b/rust/pspp/src/output/pivot/look.rs @@ -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}, diff --git a/rust/pspp/src/output/pivot/value.rs b/rust/pspp/src/output/pivot/value.rs index f71f2c733f..c5a0ebfdfc 100644 --- a/rust/pspp/src/output/pivot/value.rs +++ b/rust/pspp/src/output/pivot/value.rs @@ -21,13 +21,14 @@ // 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) -> 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) -> 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) -> 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) -> 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, 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>> { self.inner.datum() } - pub fn new_number(number: Option) -> Self { - Self::new(ValueInner::Datum(DatumValue::new_number(number))) - } - - pub fn new_integer(x: Option) -> Self { - Self::new_number(x).with_format(F40) - } - pub fn new_text(s: impl Into) -> 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) -> 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) -> Self { self.add_footnote(footnote); self } + + /// Adds `footnote` to this `Value`. pub fn add_footnote(&mut self, footnote: &Arc) { 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) -> 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) -> 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) -> 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) -> 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) -> 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>) -> 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] { 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 { + /// Returns the subscripts to be displayed, as an iterator of `&str`. + pub fn subscripts(&self) -> impl Iterator + 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> { + /// Returns the footnotes to be displayed, as an iterator of + /// [DisplayMarker]. + pub fn footnotes(&self) -> impl Iterator> + 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]) -> 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, - /// 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) -> Self { - Self::new(&Datum::<&str>::Number(number)) - } + /// Constructs a new `DatumValue` for `datum`. pub fn new(datum: &Datum) -> 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) -> 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(&self, serializer: S) -> Result 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>> { 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 { 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, + + /// Font style. pub font_style: Option, + + /// Subscripts. pub subscripts: Vec, + + /// Footnotes. pub footnotes: Vec>, } 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 { diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index 3162508801..6fb039923f 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -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, ); diff --git a/rust/pspp/src/spv/read/legacy_xml.rs b/rust/pspp/src/spv/read/legacy_xml.rs index b3bef464a6..04fbea4d69 100644 --- a/rust/pspp/src/spv/read/legacy_xml.rs +++ b/rust/pspp/src/spv/read/legacy_xml.rs @@ -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), diff --git a/rust/pspp/src/spv/read/light.rs b/rust/pspp/src/spv/read/light.rs index 21f549f9db..c557be1625 100644 --- a/rust/pspp/src/spv/read/light.rs +++ b/rust/pspp/src/spv/read/light.rs @@ -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); diff --git a/rust/pspp/src/spv/write.rs b/rust/pspp/src/spv/write.rs index 042db87885..b275f7ce2b 100644 --- a/rust/pspp/src/spv/write.rs +++ b/rust/pspp/src/spv/write.rs @@ -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; -- 2.30.2