From: Ben Pfaff Date: Wed, 12 Nov 2025 16:05:08 +0000 (-0800) Subject: work on xml spvs X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5899f2913af4914c1f65296687161cb05a46d465;p=pspp work on xml spvs --- diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 197835b05d..badb4f8ea9 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -495,6 +495,8 @@ pub struct Path<'a> { leaf: &'a Leaf, } +pub type IndexVec = SmallVec<[usize; 4]>; + impl Dimension { pub fn new(root: Group) -> Self { Dimension { @@ -521,6 +523,10 @@ impl Dimension { 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, @@ -529,6 +535,37 @@ impl Dimension { } } +/// 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)] @@ -561,7 +598,7 @@ impl Group { pub fn push(&mut self, child: impl Into) { let mut child = child.into(); - if let Category::Group(group) = &mut child { + if let Some(group) = child.as_group_mut() { group.show_label = true; } self.len += child.len(); @@ -613,6 +650,45 @@ impl Group { None } + fn index_path(&self, mut index: usize, mut path: IndexVec) -> Option { + for (i, child) in self.children.iter().enumerate() { + let len = child.len(); + if index < len { + path.push(i); + return child.index_path(index, path); + } + index -= len; + } + None + } + + fn locator_path(&self, locator: CategoryLocator) -> Option { + let mut path = self.index_path(locator.leaf_index, IndexVec::new())?; + path.truncate(path.len().checked_sub(locator.level)?); + Some(path) + } + + /// Returns `None` if `locator` is invalid (that is, if `locator.leaf_idx >= + /// self.len` or `locator.level` is greater than the depth of the leaf) or + /// if `locator` designates `self`. + pub fn category(&self, locator: CategoryLocator) -> Option<&Category> { + let path = self.locator_path(locator)?; + let mut this = &self.children[*path.get(0)?]; + for index in path[1..].iter().copied() { + this = &this.as_group().unwrap().children[index]; + } + Some(this) + } + + pub fn category_mut(&mut self, locator: CategoryLocator) -> Option<&mut Category> { + let path = self.locator_path(locator)?; + let mut this = &mut self.children[*path.get(0)?]; + for index in path[1..].iter().copied() { + this = &mut this.as_group_mut().unwrap().children[index]; + } + Some(this) + } + pub fn len(&self) -> usize { self.len } @@ -731,7 +807,7 @@ pub enum Class { Count, } -/// A pivot_category is a leaf (a category) or a group. +/// A leaf category or a group of them. #[derive(Clone, Debug, Serialize)] pub enum Category { Group(Group), @@ -739,6 +815,34 @@ pub enum Category { } impl Category { + pub fn as_group(&self) -> Option<&Group> { + match self { + Category::Group(group) => Some(group), + Category::Leaf(leaf) => None, + } + } + + pub fn as_group_mut(&mut self) -> Option<&mut Group> { + match self { + Category::Group(group) => Some(group), + Category::Leaf(leaf) => None, + } + } + + pub fn as_leaf(&self) -> Option<&Leaf> { + match self { + Category::Group(group) => None, + Category::Leaf(leaf) => Some(leaf), + } + } + + pub fn as_leaf_mut(&mut self) -> Option<&mut Leaf> { + match self { + Category::Group(group) => None, + Category::Leaf(leaf) => Some(leaf), + } + } + pub fn name(&self) -> &Value { match self { Category::Group(group) => &group.name, @@ -746,6 +850,13 @@ impl Category { } } + pub fn name_mut(&mut self) -> &mut Value { + match self { + Category::Group(group) => &mut group.name, + Category::Leaf(leaf) => &mut leaf.0, + } + } + pub fn is_empty(&self) -> bool { self.len() == 0 } @@ -760,27 +871,49 @@ impl Category { pub fn nth_leaf(&self, index: usize) -> Option<&Leaf> { match self { Category::Group(group) => group.nth_leaf(index), - Category::Leaf(leaf) => { - if index == 0 { - Some(leaf) - } else { - None - } - } + Category::Leaf(leaf) if index == 0 => Some(leaf), + _ => None, } } pub fn leaf_path<'a>(&'a self, index: usize, groups: GroupVec<'a>) -> Option> { match self { Category::Group(group) => group.leaf_path(index, groups), - Category::Leaf(leaf) => { - if index == 0 { - Some(Path { groups, leaf }) - } else { - None - } - } + Category::Leaf(leaf) if index == 0 => Some(Path { groups, leaf }), + _ => None, + } + } + + fn index_path(&self, index: usize, mut path: IndexVec) -> Option { + match self { + Category::Group(group) => group.index_path(index, path), + Category::Leaf(leaf) if index == 0 => Some(path), + _ => None, + } + } + + fn locator_path(&self, locator: CategoryLocator) -> Option { + let mut path = self.index_path(locator.leaf_index, IndexVec::new())?; + path.truncate(path.len().checked_sub(locator.level)?); + Some(path) + } + + /// Returns `None` if `locator` is invalid (that is, if `locator.leaf_idx >= + /// self.len` or `locator.level` is greater than the depth of the leaf). + pub fn category(&self, locator: CategoryLocator) -> Option<&Category> { + let mut this = self; + for index in this.locator_path(locator)? { + this = &this.as_group().unwrap().children[index]; + } + Some(this) + } + + pub fn category_mut(&mut self, locator: CategoryLocator) -> Option<&mut Category> { + let mut this = self; + for index in this.locator_path(locator)? { + this = &mut this.as_group_mut().unwrap().children[index]; } + Some(this) } pub fn show_label(&self) -> bool { @@ -1878,30 +2011,15 @@ pub trait CellIndex { I: ExactSizeIterator; } -impl CellIndex for &[usize; N] { - fn cell_index(self, dimensions: I) -> usize - where - I: ExactSizeIterator, - { - self.as_slice().cell_index(dimensions) - } -} - -impl CellIndex for [usize; N] { - fn cell_index(self, dimensions: I) -> usize - where - I: ExactSizeIterator, - { - self.as_slice().cell_index(dimensions) - } -} - -impl CellIndex for &[usize] { +impl CellIndex for T +where + T: AsRef<[usize]>, +{ fn cell_index(self, dimensions: I) -> usize where I: ExactSizeIterator, { - let data_indexes = self; + let data_indexes = self.as_ref(); let mut index = 0; for (dimension, data_index) in dimensions.zip_eq(data_indexes.iter()) { debug_assert!(*data_index < dimension); @@ -2762,15 +2880,9 @@ impl Display for DisplayValue<'_> { ValueInner::Text(TextValue { localized: local, .. }) => { - /* - if self - .inner - .styling - .as_ref() - .is_some_and(|styling| styling.style.font_style.markup) - { - todo!(); - }*/ + if self.markup { + dbg!(local); + } f.write_str(local) } diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index f42c478dac..f2b22359d5 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -481,6 +481,7 @@ struct ContainerText { impl ContainerText { fn decode(&self) -> Value { + dbg!(&self.html); html::parse(&self.html) } } diff --git a/rust/pspp/src/output/spv/html.rs b/rust/pspp/src/output/spv/html.rs index 06bc616ac7..ff9deb2467 100644 --- a/rust/pspp/src/output/spv/html.rs +++ b/rust/pspp/src/output/spv/html.rs @@ -24,7 +24,35 @@ fn find_element<'a>(elements: &'a [Node], name: &str) -> Option<&'a Element> { fn get_node_text(node: &Node, text: &mut String) { match node { - Node::Text(string) => text.push_str(&string), + Node::Text(string) => { + let mut s = string.as_str(); + 'OUTER: while !s.is_empty() { + let amp = s.find('&').unwrap_or(s.len()); + let (head, rest) = s.split_at(amp); + text.push_str(head); + if rest.is_empty() { + break; + } + + static ENTITIES: [(&str, char); 6] = [ + ("&", '&'), + ("<", '<'), + (">", '>'), + ("'", '\''), + (""", '"'), + (" ", '\u{00a0}'), + ]; + for (name, character) in ENTITIES { + if let Some(rest) = rest.strip_prefix(name) { + text.push(character); + s = rest; + continue 'OUTER; + } + } + text.push('&'); + s = &s[1..]; + } + } Node::Element(element) => get_element_text(element, text), Node::Comment(_) => (), } @@ -39,6 +67,7 @@ fn get_element_text(element: &Element, text: &mut String) { fn extract_html_text(node: &Node, base_font_size: i32, s: &mut String) { match node { Node::Text(text) => { + dbg!(text); for c in text.chars() { fn push_whitespace(c: char, s: &mut String) { if s.chars().next_back().is_none_or(|c| !c.is_whitespace()) { @@ -62,7 +91,6 @@ fn extract_html_text(node: &Node, base_font_size: i32, s: &mut String) { _ if c.is_whitespace() => push_whitespace(c, s), '<' => s.push_str("<"), '>' => s.push_str(">"), - '&' => s.push_str("&"), _ => s.push(c), } } @@ -350,6 +378,7 @@ pub fn parse(input: &str) -> Value { extract_html_text(node, font_style.size, &mut s); } } + dbg!(&s); s } _ => input.into(), @@ -359,6 +388,8 @@ pub fn parse(input: &str) -> Value { #[cfg(test)] mod tests { + use html_parser::Dom; + use crate::output::{ pivot::{FontStyle, Value}, spv::html::{parse, parse_paragraphs, parse_value}, @@ -396,6 +427,13 @@ mod tests { ); } + #[test] + fn entity() { + let html = r#"Hi there!"#; + dbg!(Dom::parse(html)); + todo!() + } + #[test] fn paragraphs() { let paragraphs = parse_paragraphs( diff --git a/rust/pspp/src/output/spv/legacy_bin.rs b/rust/pspp/src/output/spv/legacy_bin.rs index dd9a2ccb26..a96ca10e66 100644 --- a/rust/pspp/src/output/spv/legacy_bin.rs +++ b/rust/pspp/src/output/spv/legacy_bin.rs @@ -105,9 +105,8 @@ impl DataValue { } pub fn as_pivot_value(&self, format: Format) -> Value { - dbg!(format.type_()); if format.type_().category() == Category::Date - && let Some(s) = self.value.as_string() + && let Some(s) = dbg!(self.value.as_string()) && let Ok(date_time) = NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S%.3f") { diff --git a/rust/pspp/src/output/spv/legacy_xml.rs b/rust/pspp/src/output/spv/legacy_xml.rs index 2bfec0fbc3..08509304f5 100644 --- a/rust/pspp/src/output/spv/legacy_xml.rs +++ b/rust/pspp/src/output/spv/legacy_xml.rs @@ -15,26 +15,34 @@ // this program. If not, see . use std::{ + cell::{Cell, RefCell}, collections::{BTreeMap, HashMap}, marker::PhantomData, mem::take, num::NonZeroUsize, + ops::Range, + sync::Arc, }; +use chrono::{NaiveDateTime, NaiveTime}; use enum_map::{Enum, EnumMap}; +use hashbrown::HashSet; +use itertools::Itertools; use ordered_float::OrderedFloat; use serde::Deserialize; use crate::{ + calendar::{date_time_to_pspp, time_to_pspp}, data::Datum, - format::{Decimal::Dot, F8_0, F40_2, Type, UncheckedFormat}, + format::{self, Decimal::Dot, F8_0, F40_2, Type, UncheckedFormat}, output::{ pivot::{ - self, Area, AreaStyle, Axis2, Axis3, Category, Color, Dimension, Group, HeadingRegion, - HorzAlign, Leaf, Length, Look, NumberValue, PivotTable, RowParity, Value, ValueInner, - VertAlign, + self, Area, AreaStyle, Axis2, Axis3, Category, CategoryLocator, CellStyle, Color, + Dimension, Group, HeadingRegion, HorzAlign, Leaf, Length, Look, NumberValue, + PivotTable, RowParity, Value, ValueInner, VertAlign, }, spv::legacy_bin::DataValue, + table, }, }; @@ -241,9 +249,14 @@ impl Visualization { }; let mut axes = HashMap::new(); + let mut major_ticks = HashMap::new(); for child in &graph.facet_layout.children { if let FacetLayoutChild::FacetLevel(facet_level) = child { axes.insert(facet_level.level, &facet_level.axis); + major_ticks.insert( + facet_level.axis.major_ticks.id.as_str(), + &facet_level.axis.major_ticks, + ); } } @@ -305,10 +318,12 @@ impl Visualization { label.decode_style(&mut look.areas[area], &styles); } } + let title = LabelFrame::decode_label(&labels[Purpose::Title]); + let caption = LabelFrame::decode_label(&labels[Purpose::SubTitle]); if let Some(style) = &graph.interval.labeling.style && let Some(style) = styles.get(style.references.as_str()) { - Style::decode( + Style::decode_area( Some(*style), graph.cell_style.get(&styles), &mut look.areas[Area::Data(RowParity::Even)], @@ -317,10 +332,6 @@ impl Visualization { look.areas[Area::Data(RowParity::Even)].clone(); } - let mut _title = Value::empty(); - let mut _caption = Value::empty(); - //Label::decode_ - let _show_grid_lines = extension .as_ref() .and_then(|extension| extension.show_gridline); @@ -386,7 +397,7 @@ impl Visualization { let out = &mut look.areas[Area::Labels(a)]; *out = Area::Labels(a).default_area_style(); let style = label.style.get(&styles); - Style::decode( + Style::decode_area( style, label.text_frame_style.as_ref().and_then(|r| r.get(styles)), out, @@ -398,7 +409,7 @@ impl Visualization { if a == Axis3::Y && let Some(axis) = axes.get(&(base_level + variables.len() - 1)) { - Style::decode( + Style::decode_area( axis.major_ticks.style.get(&styles), axis.major_ticks.tick_frame_style.get(&styles), &mut look.areas[Area::Labels(Axis2::Y)], @@ -420,6 +431,21 @@ impl Visualization { .map(|(series, _level)| *series) .collect::>(); + #[derive(Clone)] + struct CatBuilder { + /// The category we've built so far. + category: Category, + + /// The range of leaf indexes covered by `category`. + /// + /// If `category` is a leaf, the range has a length of 1. + /// If `category` is a group, the length is at least 1. + leaves: Range, + + /// How to find this category in its dimension. + location: CategoryLocator, + } + // Make leaf categories. let mut coordinate_to_index = HashMap::new(); let mut cats = Vec::new(); @@ -427,40 +453,51 @@ impl Visualization { let Some(row) = value.category() else { continue; }; - coordinate_to_index.insert(row, index); + coordinate_to_index.insert(row, CategoryLocator::new_leaf(index)); let name = variables[0].new_name(value, footnotes); - cats.push((Category::from(Leaf::new(name)), cats.len()..cats.len() + 1)); + cats.push(CatBuilder { + category: Category::from(Leaf::new(name)), + leaves: cats.len()..cats.len() + 1, + location: CategoryLocator::new_leaf(cats.len()), + }); } + *variables[0].coordinate_to_index.borrow_mut() = coordinate_to_index; // Now group them, in one pass per grouping variable, innermost first. for j in 1..variables.len() { + let mut coordinate_to_index = HashMap::new(); let mut next_cats = Vec::with_capacity(cats.len()); let mut start = 0; for end in 1..=cats.len() { - let dv1 = &variables[j].values[cats[start].1.start]; + let dv1 = &variables[j].values[cats[start].leaves.start]; if end < cats.len() - && variables[j].values[cats[end].1.clone()] + && variables[j].values[cats[end].leaves.clone()] .iter() .all(|dv| &dv.value == &dv1.value) { } else { let name = variables[j].map.lookup(dv1); - if end - start > 1 || name.is_number_or(|s| s.is_empty()) { + let next_cat = if end - start > 1 || name.is_number_or(|s| s.is_empty()) { let name = variables[j].new_name(dv1, footnotes); let mut group = Group::new(name); for i in start..end { - group.push(cats[i].0.clone()); + group.push(cats[i].category.clone()); + } + CatBuilder { + category: Category::from(group), + leaves: cats[start].leaves.start..cats[end - 1].leaves.end, + location: cats[start].location.parent(), } - next_cats.push(( - Category::from(group), - cats[start].1.start..cats[end - 1].1.end, - )); } else { - next_cats.push(cats[start].clone()); - } + cats[start].clone() + }; + coordinate_to_index + .insert(dv1.category().unwrap() /*XXX?*/, next_cat.location); + next_cats.push(next_cat); start = end; } } + *variables[j].coordinate_to_index.borrow_mut() = coordinate_to_index; cats = next_cats; } @@ -471,14 +508,17 @@ impl Visualization { .as_ref() .map_or_else(|| Value::empty(), |label| Value::new_user_text(label)), ) - .with_multiple(cats.into_iter().map(|(category, _range)| category)) + .with_multiple(cats.into_iter().map(|cb| cb.category)) .with_show_label(show_label), ); + + for variable in &variables { + variable.dimension_index.set(Some(dims.len())); + } dims.push(Dim { axis: a, - dimension: Some(dimension), + dimension, coordinate: variables[0], - coordinate_to_index, }); } @@ -541,9 +581,8 @@ impl Visualization { struct Dim<'a> { axis: Axis3, - dimension: Option, + dimension: pivot::Dimension, coordinate: &'a Series, - coordinate_to_index: HashMap, } let mut rotate_inner_column_labels = false; @@ -603,13 +642,6 @@ impl Visualization { level_ofs += layers.len(); } - let dimensions = dims - .iter_mut() - .map(|dim| (dim.axis, dim.dimension.take().unwrap())) - .collect::>(); - - let mut pivot_table = PivotTable::new(dimensions); - let cell = series.get("cell").unwrap()/*XXX*/; let mut coords = Vec::with_capacity(dims.len()); let (cell_formats, format_map) = graph.interval.labeling.decode_format_map(&series); @@ -623,14 +655,20 @@ impl Visualization { LabelingChild::Footnotes(footnotes) => series.get(footnotes.variable.as_str()), _ => None, }); + let mut data = HashMap::new(); for (i, cell) in cell.values.iter().enumerate() { coords.clear(); for dim in &dims { // XXX indexing of values, and unwrap let coordinate = dim.coordinate.values[i].category().unwrap(); - let index = match dim.coordinate_to_index.get(&coordinate) { - Some(index) => *index, - None => panic!("can't find {coordinate}"), + let Some(index) = dim + .coordinate + .coordinate_to_index + .borrow() + .get(&coordinate) + .and_then(CategoryLocator::as_leaf) + else { + panic!("can't find {coordinate}") // XXX }; coords.push(index); } @@ -665,25 +703,342 @@ impl Visualization { // A system-missing value without a footnote represents an empty cell. } else { // XXX cell_index might be invalid? - pivot_table.insert(coords.as_slice(), value); + data.insert(coords.clone(), value); } } - // XXX decode_set_cell_properties + for child in &graph.facet_layout.children { + let FacetLayoutChild::SetCellProperties(scp) = child else { + continue; + }; + + #[derive(Copy, Clone, Debug, PartialEq)] + enum TargetType { + Graph, + Labeling, + Interval, + MajorTicks, + } + + impl TargetType { + fn from_id( + target: &str, + graph: &Graph, + major_ticks: &HashMap<&str, &MajorTicks>, + ) -> Option { + if let Some(id) = &graph.id + && id == target + { + Some(Self::Graph) + } else if let Some(id) = &graph.interval.labeling.id + && id == target + { + Some(Self::Labeling) + } else if let Some(id) = &graph.interval.id + && id == target + { + Some(Self::Interval) + } else if major_ticks.contains_key(target) { + Some(Self::MajorTicks) + } else { + None + } + } + } + + #[derive(Default)] + struct Target<'a> { + graph: Option<&'a Style>, + labeling: Option<&'a Style>, + interval: Option<&'a Style>, + major_ticks: Option<&'a Style>, + frame: Option<&'a Style>, + format: Option<(&'a SetFormat, Option)>, + } + impl<'a> Target<'a> { + fn decode( + &self, + intersect: &Intersect, + look: &mut Look, + series: &HashMap<&str, Series>, + dims: &mut [Dim], + data: &mut HashMap, Value>, + ) { + let mut wheres = Vec::new(); + let mut alternating = false; + for child in &intersect.children { + match child { + IntersectChild::Where(w) => wheres.push(w), + IntersectChild::IntersectWhere(_) => { + // Presumably we should do something (but we don't). + } + IntersectChild::Alternating => alternating = true, + IntersectChild::Empty => (), + } + } + + match self { + Self { + graph: Some(_), + labeling: Some(_), + interval: None, + major_ticks: None, + frame: None, + format: None, + } if alternating => { + let mut style = Area::Data(RowParity::Odd).default_area_style(); + Style::decode_area(self.labeling, self.graph, &mut style); + let font_style = &mut look.areas[Area::Data(RowParity::Odd)].font_style; + font_style.fg = style.font_style.fg; + font_style.bg = style.font_style.bg; + } + Self { + graph: Some(_), + labeling: None, + interval: None, + major_ticks: None, + frame: None, + format: None, + } => { + // `graph.width` likely just sets the width of the table as a whole. + } + Self { + graph: None, + labeling: None, + interval: None, + major_ticks: None, + frame: None, + format: None, + } => { + // No-op. (Presumably there's a setMetaData we don't care about.) + } + Self { + format: Some((_, Some(TargetType::MajorTicks))), + .. + } + | Self { + major_ticks: Some(_), + .. + } + | Self { frame: Some(_), .. } + if !wheres.is_empty() => + { + // Formatting for individual row or column labels. + for w in &wheres { + let Some(s) = series.get(w.variable.as_str()) else { + continue; + }; + let Some(dim_index) = s.dimension_index.get() else { + continue; + }; + let dimension = &mut dims[dim_index].dimension; + let Ok(axis) = Axis2::try_from(dims[dim_index].axis) else { + continue; + }; + for index in + w.include.split(';').filter_map(|s| s.parse::().ok()) + { + if let Some(locator) = + s.coordinate_to_index.borrow().get(&index).copied() + && let Some(category) = dimension.root.category_mut(locator) + { + Style::apply_to_value( + category.name_mut(), + self.format.map(|(sf, _)| sf), + self.major_ticks, + self.frame, + &look.areas[Area::Labels(axis)], + ); + } + } + } + } + Self { + format: Some((_, Some(TargetType::Labeling))), + .. + } + | Self { + labeling: Some(_), .. + } + | Self { + interval: Some(_), .. + } => { + // Formatting for individual cells or groups of them + // with some dimensions in common. + let mut include = vec![HashSet::new(); dims.len()]; + for w in &wheres { + let Some(s) = series.get(w.variable.as_str()) else { + continue; + }; + let Some(dim_index) = s.dimension_index.get() else { + // Group indexes may be included even though + // they are redundant. Ignore them. + continue; + }; + let dimension = &mut dims[dim_index].dimension; + for index in + w.include.split(';').filter_map(|s| s.parse::().ok()) + { + if let Some(locator) = + s.coordinate_to_index.borrow().get(&index).copied() + && let Some(leaf_index) = locator.as_leaf() + { + include[dim_index].insert(leaf_index); + } + } + } + + // XXX This is inefficient in the common case where + // all of the dimensions are matched. We should use + // a heuristic where if all of the dimensions are + // matched and the product of n[*] is less than the + // number of cells then iterate through all the + // possibilities rather than all the cells. Or even + // only do it if there is just one possibility. + for (indexes, value) in data { + let mut skip = false; + for (dimension, index) in indexes.iter().enumerate() { + if !include[dimension].is_empty() + && !include[dimension].contains(index) + { + skip = true; + break; + } + } + if !skip { + Style::apply_to_value( + value, + self.format.map(|(sf, _)| sf), + self.major_ticks, + self.frame, + &look.areas[Area::Data(RowParity::Even)], + ); + } + } + } + _ => (), + } + } + } + let mut target = Target::default(); + for set in &scp.sets { + match set { + Set::SetStyle(set_style) => { + if let Some(style) = set_style.style.get(&styles) { + match TargetType::from_id(&set_style.target, graph, &major_ticks) { + Some(TargetType::Graph) => target.graph = Some(style), + Some(TargetType::Interval) => target.interval = Some(style), + Some(TargetType::Labeling) => target.labeling = Some(style), + Some(TargetType::MajorTicks) => target.major_ticks = Some(style), + None => (), + } + } + } + Set::SetFrameStyle(set_frame_style) => { + target.frame = set_frame_style.style.get(&styles) + } + Set::SetFormat(sf) => { + let target_type = TargetType::from_id(&sf.target, graph, &major_ticks); + target.format = Some((sf, target_type)) + } + Set::SetMetaData(_) => (), + } + } + + match ( + scp.union_.as_ref(), + scp.apply_to_converse.unwrap_or_default(), + ) { + (Some(union_), false) => { + for intersect in &union_.intersects { + target.decode( + intersect, + &mut look, + &series, + dims.as_mut_slice(), + &mut data, + ); + } + } + (Some(_), true) => { + // Not implemented, not seen in the corpus. + } + (None, true) => { + if target + .format + .is_some_and(|(_sf, target_type)| target_type == Some(TargetType::Labeling)) + || target.labeling.is_some() + || target.interval.is_some() + { + for value in data.values_mut() { + Style::apply_to_value( + value, + target.format.map(|(sf, _target_type)| sf), + None, + None, + &look.areas[Area::Data(RowParity::Even)], + ); + } + } + } + (None, false) => { + // Appears to be used to set the font for something—but what? + } + } + } + + let dimensions = dims + .into_iter() + .map(|dim| (dim.axis, dim.dimension)) + .collect::>(); + let mut pivot_table = PivotTable::new(dimensions) + .with_look(Arc::new(look)) + .with_data(data); + if let Some(title) = title { + pivot_table = pivot_table.with_title(title); + } + if let Some(caption) = caption { + pivot_table = pivot_table.with_caption(caption); + } Ok(pivot_table) } } struct Series { + name: String, label: Option, format: crate::format::Format, remapped: bool, values: Vec, map: Map, affixes: Vec, + coordinate_to_index: RefCell>, + dimension_index: Cell>, } impl Series { + fn new(name: String, values: Vec, map: Map) -> Self { + Self { + name, + label: None, + format: F8_0, + remapped: false, + values, + map, + affixes: Vec::new(), + coordinate_to_index: Default::default(), + dimension_index: Default::default(), + } + } + fn with_format(self, format: crate::format::Format) -> Self { + Self { format, ..self } + } + fn with_label(self, label: Option) -> Self { + Self { label, ..self } + } + fn with_affixes(self, affixes: Vec) -> Self { + Self { affixes, ..self } + } fn add_affixes(&self, mut value: Value, footnotes: &pivot::Footnotes) -> Value { for affix in &self.affixes { if let Some(index) = affix.defines_reference.checked_sub(1) @@ -798,14 +1153,10 @@ impl SourceVariable { } else if let Some(label_series) = label_series { map.insert_labels(&data, label_series, format); } - Ok(Series { - label: self.label.clone(), - format, - remapped: false, - values: data, - map, - affixes, - }) + Ok(Series::new(self.id.clone(), data, map) + .with_format(format) + .with_affixes(affixes) + .with_label(self.label.clone())) } } @@ -866,14 +1217,7 @@ impl DerivedVariable { { values.clear(); } - Ok(Series { - label: None, - format: F8_0, - remapped: false, - values, - map, - affixes: Vec::new(), - }) + Ok(Series::new(self.id.clone(), values, map)) } } @@ -1507,43 +1851,131 @@ struct Style { } impl Style { - fn decode(fg: Option<&Style>, bg: Option<&Style>, out: &mut AreaStyle) { + fn apply_to_value( + value: &mut Value, + sf: Option<&SetFormat>, + fg: Option<&Style>, + bg: Option<&Style>, + base_style: &AreaStyle, + ) { + if let Some(sf) = sf { + if sf.reset == Some(true) { + value.styling_mut().footnotes.clear(); + } + + let format = match &sf.child { + Some(SetFormatChild::Format(format)) => Some(format.decode()), + Some(SetFormatChild::NumberFormat(format)) => { + Some(SignificantNumberFormat::from(format).decode()) + } + Some(SetFormatChild::StringFormat(format)) => None, + Some(SetFormatChild::DateTimeFormat(format)) => Some(format.decode()), + Some(SetFormatChild::ElapsedTimeFormat(format)) => Some(format.decode()), + None => None, + }; + if let Some(format) = format { + match &mut value.inner { + ValueInner::Number(number) => { + number.format = format; + } + ValueInner::String(string) => { + if format.type_().category() == format::Category::Date + && let Ok(date_time) = + NaiveDateTime::parse_from_str(&string.s, "%Y-%m-%dT%H:%M:%S%.3f") + { + value.inner = ValueInner::Number(NumberValue { + show: None, + format, + honor_small: false, + value: Some(date_time_to_pspp(date_time)), + variable: None, + value_label: None, + }) + } else if format.type_().category() == format::Category::Time + && let Ok(time) = NaiveTime::parse_from_str(&string.s, "%H:%M:%S%.3f") + { + value.inner = ValueInner::Number(NumberValue { + show: None, + format, + honor_small: false, + value: Some(time_to_pspp(time)), + variable: None, + value_label: None, + }) + } else if let Ok(number) = string.s.parse::() { + value.inner = ValueInner::Number(NumberValue { + show: None, + format, + honor_small: false, + value: Some(number), + variable: None, + value_label: None, + }) + } + } + _ => (), + } + } + } + + if fg.is_some() || bg.is_some() { + let mut styling = value.styling_mut(); + let font_style = styling + .font_style + .get_or_insert_with(|| base_style.font_style.clone()); + let cell_style = styling + .cell_style + .get_or_insert_with(|| base_style.cell_style.clone()); + Self::decode(fg, bg, cell_style, font_style); + } + } + + fn decode( + fg: Option<&Style>, + bg: Option<&Style>, + cell_style: &mut CellStyle, + font_style: &mut pivot::FontStyle, + ) { if let Some(fg) = fg { if let Some(weight) = fg.font_weight { - out.font_style.bold = weight.is_bold(); + font_style.bold = weight.is_bold(); } if let Some(style) = fg.font_style { - out.font_style.italic = style.is_italic(); + font_style.italic = style.is_italic(); } if let Some(underline) = fg.font_underline { - out.font_style.underline = underline.is_underline(); + font_style.underline = underline.is_underline(); } if let Some(color) = fg.color { - out.font_style.fg = color; + font_style.fg = color; } if let Some(font_size) = &fg.font_size { if let Ok(size) = font_size .trim_end_matches(|c: char| c.is_alphabetic()) .parse() { - out.font_style.size = size; + font_style.size = size; } else { // XXX warn? } } if let Some(alignment) = fg.text_alignment { - out.cell_style.horz_align = alignment.as_horz_align(fg.decimal_offset); + cell_style.horz_align = alignment.as_horz_align(fg.decimal_offset); } if let Some(label_local_vertical) = fg.label_location_vertical { - out.cell_style.vert_align = label_local_vertical.into(); + cell_style.vert_align = label_local_vertical.into(); } } if let Some(bg) = bg { if let Some(color) = bg.color { - out.font_style.bg = color; + font_style.bg = color; } } } + + fn decode_area(fg: Option<&Style>, bg: Option<&Style>, out: &mut AreaStyle) { + Self::decode(fg, bg, &mut out.cell_style, &mut out.font_style); + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] @@ -1641,6 +2073,9 @@ impl From for VertAlign { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct Graph { + #[serde(rename = "@id")] + id: Option, + #[serde(rename = "@cellStyle")] cell_style: Ref