(date_time - EPOCH_DATETIME).as_seconds_f64()
}
+pub fn time_to_pspp(time: NaiveTime) -> f64 {
+ (time - NaiveTime::MIN).as_seconds_f64()
+}
+
/// Takes a count of days from 14 Oct 1582 and translates it into a Gregorian
/// calendar date, if possible. Positive and negative offsets are supported.
pub fn calendar_offset_to_gregorian(offset: f64) -> Option<NaiveDate> {
endian: Endian,
args: Self::Args<'_>,
) -> binrw::BinResult<()> {
- (self.0.len() as u32).write_options(writer, endian, args)?;
- for footnote in &self.0 {
+ (self.len() as u32).write_options(writer, endian, args)?;
+ for footnote in self {
footnote.write_options(writer, endian, args)?;
}
Ok(())
use enum_iterator::Sequence;
use enum_map::{Enum, EnumMap, enum_map};
use itertools::Itertools;
-pub use look_xml::TableProperties;
+pub use look_xml::{Length, TableProperties};
use quick_xml::{DeError, de::from_str};
use serde::{
Deserialize, Serialize, Serializer,
}
#[derive(Clone, Debug, Default, Serialize)]
-pub struct Footnotes(pub Vec<Arc<Footnote>>);
+pub struct Footnotes(Vec<Arc<Footnote>>);
impl Footnotes {
pub fn new() -> Self {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
+
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ pub fn get(&self, index: usize) -> Option<&Arc<Footnote>> {
+ self.0.get(index)
+ }
+}
+
+impl Index<usize> for Footnotes {
+ type Output = Arc<Footnote>;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.0[index]
+ }
+}
+
+impl<'a> IntoIterator for &'a Footnotes {
+ type Item = &'a Arc<Footnote>;
+
+ type IntoIter = std::slice::Iter<'a, Arc<Footnote>>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.iter()
+ }
}
impl FromIterator<Footnote> for Footnotes {
/// The division between this and the style information in [PivotTable] seems
/// 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)]
+#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Look {
pub name: Option<String>,
}
}
-#[derive(Clone, Debug, Serialize)]
+#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct AreaStyle {
pub cell_style: CellStyle,
pub font_style: FontStyle,
}
}
-#[derive(Copy, Clone, Debug, Deserialize)]
+#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
pub struct BorderStyle {
#[serde(rename = "@borderStyleType")]
pub stroke: Stroke,
}
}
+impl Default for Footnote {
+ fn default() -> Self {
+ Footnote::new(Value::default())
+ }
+}
+
pub struct DisplayMarker<'a> {
footnote: &'a Footnote,
options: ValueOptions,
Datum::String(string) => Self::new_user_text(string.as_str()),
}
}
- pub fn new_variable_value(variable: &Variable, value: &Datum<ByteString>) -> Self {
- let var_name = Some(variable.name.as_str().into());
- let value_label = variable.value_labels.get(value).map(String::from);
+ pub fn new_datum_with_format<B>(value: &Datum<B>, format: Format) -> Self
+ where
+ B: EncodedString,
+ {
match value {
Datum::Number(number) => Self::new(ValueInner::Number(NumberValue {
show: None,
- format: match variable.print_format.var_type() {
- VarType::Numeric => variable.print_format,
+ format: match format.var_type() {
+ VarType::Numeric => format,
VarType::String => {
#[cfg(debug_assertions)]
panic!("cannot create numeric pivot value with string format");
},
honor_small: false,
value: *number,
- variable: var_name,
- value_label,
+ variable: None,
+ value_label: None,
})),
Datum::String(string) => Self::new(ValueInner::String(StringValue {
show: None,
- hex: variable.print_format.type_() == Type::AHex,
- s: string
- .as_ref()
- .with_encoding(variable.encoding())
- .into_string(),
- var_name,
- value_label,
+ hex: format.type_() == Type::AHex,
+ s: string.as_str().into_owned(),
+ var_name: None,
+ value_label: None,
})),
}
}
+ pub fn new_variable_value(variable: &Variable, value: &Datum<ByteString>) -> Self {
+ Self::new_datum_with_format(
+ &value.as_encoded(variable.encoding()),
+ variable.print_format,
+ )
+ .with_variable_name(Some(variable.name.as_str().into()))
+ .with_value_label(variable.value_labels.get(value).map(String::from))
+ }
pub fn new_number(x: Option<f64>) -> Self {
Self::new_number_with_format(x, F8_2)
}
#[serde(rename = "@font-family")]
font_family: String,
#[serde(rename = "@font-size")]
- font_size: Dimension,
+ font_size: Length,
#[serde(rename = "@font-style")]
font_style: FontStyle,
#[serde(rename = "@font-weight")]
#[serde(rename = "@labelLocationVertical")]
label_location_vertical: LabelLocationVertical,
#[serde(rename = "@margin-bottom")]
- margin_bottom: Dimension,
+ margin_bottom: Length,
#[serde(rename = "@margin-left")]
- margin_left: Dimension,
+ margin_left: Length,
#[serde(rename = "@margin-right")]
- margin_right: Dimension,
+ margin_right: Length,
#[serde(rename = "@margin-top")]
- margin_top: Dimension,
+ margin_top: Length,
#[serde(rename = "@textAlignment", default)]
text_alignment: TextAlignment,
#[serde(rename = "@decimal-offset")]
- decimal_offset: Dimension,
+ decimal_offset: Length,
}
impl CellStyle {
}
#[derive(Copy, Clone, Default, PartialEq)]
-pub struct Dimension(
+pub struct Length(
/// In inches.
- f64,
+ pub f64,
);
-impl Dimension {
- fn as_px_f64(self) -> f64 {
+impl Length {
+ pub fn as_px_f64(self) -> f64 {
self.0 * 96.0
}
- fn as_px_i32(self) -> i32 {
+ pub fn as_px_i32(self) -> i32 {
num::cast(self.as_px_f64() + 0.5).unwrap_or_default()
}
- fn as_pt_f64(self) -> f64 {
+ pub fn as_pt_f64(self) -> f64 {
self.0 * 72.0
}
- fn as_pt_i32(self) -> i32 {
+ pub fn as_pt_i32(self) -> i32 {
num::cast(self.as_pt_f64() + 0.5).unwrap_or_default()
}
}
-impl Debug for Dimension {
+impl Debug for Length {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.2}in", self.0)
}
}
-impl FromStr for Dimension {
- type Err = DimensionParseError;
+impl FromStr for Length {
+ type Err = LengthParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim_start();
let unit = s.trim_start_matches(|c: char| c.is_ascii_digit() || c == '.');
let number: f64 = s[..s.len() - unit.len()]
.parse()
- .map_err(DimensionParseError::ParseFloatError)?;
+ .map_err(LengthParseError::ParseFloatError)?;
let divisor = match unit.trim() {
// Inches.
"in" | "인치" | "pol." | "cala" | "cali" => 1.0,
// Centimeters.
"cm" | "см" => 2.54,
- other => return Err(DimensionParseError::InvalidUnit(other.into())),
+ other => return Err(LengthParseError::InvalidUnit(other.into())),
};
- Ok(Dimension(number / divisor))
+ Ok(Length(number / divisor))
}
}
#[derive(ThisError, Debug, PartialEq, Eq)]
-enum DimensionParseError {
+pub enum LengthParseError {
/// Invalid number.
#[error(transparent)]
ParseFloatError(ParseFloatError),
InvalidUnit(String),
}
-impl<'de> Deserialize<'de> for Dimension {
+impl<'de> Deserialize<'de> for Length {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
struct DimensionVisitor;
impl<'de> Visitor<'de> for DimensionVisitor {
- type Value = Dimension;
+ type Value = Length;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
- formatter.write_str("a string")
+ formatter.write_str("a dimension expressed as a string, e.g. \"1.0 cm\"")
}
- fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
mod tests {
use std::str::FromStr;
+ use enum_map::{EnumMap, enum_map};
use quick_xml::de::from_str;
- use crate::output::pivot::look_xml::{Dimension, DimensionParseError, TableProperties};
+ use crate::output::pivot::{
+ Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, CellStyle, Color, FontStyle,
+ FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look,
+ RowColBorder, RowParity, Stroke, VertAlign,
+ look_xml::{Length, LengthParseError, TableProperties},
+ };
#[test]
fn dimension() {
- assert_eq!(Dimension::from_str("1"), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str("1pt"), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str("1пт"), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str("1.0"), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str(" 1.0"), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str(" 1.0 "), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str("1.0 pt"), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str("1.0pt "), Ok(Dimension(1.0 / 72.0)));
- assert_eq!(Dimension::from_str(" 1.0pt "), Ok(Dimension(1.0 / 72.0)));
+ assert_eq!(Length::from_str("1"), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str("1pt"), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str("1пт"), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str("1.0"), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str(" 1.0"), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str(" 1.0 "), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str("1.0 pt"), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str("1.0pt "), Ok(Length(1.0 / 72.0)));
+ assert_eq!(Length::from_str(" 1.0pt "), Ok(Length(1.0 / 72.0)));
- assert_eq!(Dimension::from_str("1in"), Ok(Dimension(1.0)));
+ assert_eq!(Length::from_str("1in"), Ok(Length(1.0)));
- assert_eq!(Dimension::from_str("96px"), Ok(Dimension(1.0)));
+ assert_eq!(Length::from_str("96px"), Ok(Length(1.0)));
- assert_eq!(Dimension::from_str("2.54cm"), Ok(Dimension(1.0)));
+ assert_eq!(Length::from_str("2.54cm"), Ok(Length(1.0)));
assert_eq!(
- Dimension::from_str(""),
- Err(DimensionParseError::ParseFloatError(
+ Length::from_str(""),
+ Err(LengthParseError::ParseFloatError(
"".parse::<f64>().unwrap_err()
))
);
assert_eq!(
- Dimension::from_str("1.2.3"),
- Err(DimensionParseError::ParseFloatError(
+ Length::from_str("1.2.3"),
+ Err(LengthParseError::ParseFloatError(
"1.2.3".parse::<f64>().unwrap_err()
))
);
assert_eq!(
- Dimension::from_str("1asdf"),
- Err(DimensionParseError::InvalidUnit("asdf".into()))
+ Length::from_str("1asdf"),
+ Err(LengthParseError::InvalidUnit("asdf".into()))
);
}
</tableProperties>
"##;
let table_properties: TableProperties = from_str(XML).unwrap();
- dbg!(&table_properties);
- todo!()
+ let look: Look = table_properties.into();
+ dbg!(&look);
+ let expected = Look {
+ name: None,
+ hide_empty: true,
+ row_label_position: LabelPosition::Corner,
+ heading_widths: enum_map! {
+ HeadingRegion::Rows => 36..=120,
+ HeadingRegion::Columns => 36..=72,
+ },
+ footnote_marker_type: FootnoteMarkerType::Alphabetic,
+ footnote_marker_position: FootnoteMarkerPosition::Subscript,
+ areas: enum_map! {
+ Area::Title => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Left,
+ ),
+ vert_align: VertAlign::Middle,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 8,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: true,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Caption => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Left,
+ ),
+ vert_align: VertAlign::Top,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 0,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Footer => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Left,
+ ),
+ vert_align: VertAlign::Top,
+ margins: enum_map! {
+ Axis2::X => [
+ 11,
+ 8,
+ ],
+ Axis2::Y => [
+ 1,
+ 3,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Corner => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Left,
+ ),
+ vert_align: VertAlign::Bottom,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 0,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Labels(
+ Axis2::X,
+ ) => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Center,
+ ),
+ vert_align: VertAlign::Bottom,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 3,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Labels(
+ Axis2::Y,
+ )=> AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Left,
+ ),
+ vert_align: VertAlign::Top,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 3,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Data(
+ RowParity::Even,
+ ) => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: None,
+ vert_align: VertAlign::Top,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 0,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ Area::Data(
+ RowParity::Odd,
+ )=>AreaStyle {
+ cell_style: CellStyle {
+ horz_align: None,
+ vert_align: VertAlign::Top,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 0,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::BLACK,
+ size: 9,
+ },
+ },
+ Area::Layers => AreaStyle {
+ cell_style: CellStyle {
+ horz_align: Some(
+ HorzAlign::Left,
+ ),
+ vert_align: VertAlign::Bottom,
+ margins: enum_map! {
+ Axis2::X => [
+ 8,
+ 11,
+ ],
+ Axis2::Y => [
+ 0,
+ 3,
+ ],
+ },
+ },
+ font_style: FontStyle {
+ bold: false,
+ italic: false,
+ underline: false,
+ markup: false,
+ font: String::from("Sans Serif"),
+ fg: Color::BLACK,
+ bg: Color::WHITE,
+ size: 9,
+ },
+ },
+ },
+ borders: enum_map! {
+ Border::Title => BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::OuterFrame(
+ BoxBorder::Left,
+ )=>BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::OuterFrame(
+ BoxBorder::Top,
+ ) =>BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::OuterFrame(
+ BoxBorder::Right,
+ ) => BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::OuterFrame(
+ BoxBorder::Bottom,
+ )=> BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::InnerFrame(
+ BoxBorder::Left,
+ )=> BorderStyle {
+ stroke: Stroke::Thick,
+ color: Color::BLACK,
+ },
+ Border::InnerFrame(
+ BoxBorder::Top,
+ )=> BorderStyle {
+ stroke: Stroke::Thick,
+ color: Color::BLACK,
+ },
+ Border::InnerFrame(
+ BoxBorder::Right,
+ )=> BorderStyle {
+ stroke: Stroke::Thick,
+ color: Color::BLACK,
+ },
+ Border::InnerFrame(
+ BoxBorder::Bottom,
+ )=> BorderStyle {
+ stroke: Stroke::Thick,
+ color: Color::BLACK,
+ },
+ Border::Dimension(
+ RowColBorder(
+ HeadingRegion::Rows,
+ Axis2::X,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::Solid,
+ color: Color::BLACK,
+ },
+ Border::Dimension(
+ RowColBorder(
+ HeadingRegion::Columns,
+ Axis2::X,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::Solid,
+ color: Color::BLACK,
+ },
+ Border::Dimension(
+ RowColBorder(
+ HeadingRegion::Rows,
+ Axis2::Y,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::Dimension(
+ RowColBorder(
+ HeadingRegion::Columns,
+ Axis2::Y,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::Solid,
+ color: Color::BLACK,
+ },
+ Border::Category(
+ RowColBorder(
+ HeadingRegion::Rows,
+ Axis2::X,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::Category(
+ RowColBorder(
+ HeadingRegion::Columns,
+ Axis2::X,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::Solid,
+ color: Color::BLACK,
+ },
+ Border::Category(
+ RowColBorder(
+ HeadingRegion::Rows,
+ Axis2::Y,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::None,
+ color: Color::BLACK,
+ },
+ Border::Category(
+ RowColBorder(
+ HeadingRegion::Columns,
+ Axis2::Y,
+ ),
+ )=> BorderStyle {
+ stroke: Stroke::Solid,
+ color: Color::BLACK,
+ },
+ Border::DataLeft => BorderStyle {
+ stroke: Stroke::Thick,
+ color: Color::BLACK,
+ },
+ Border::DataTop => BorderStyle {
+ stroke: Stroke::Thick,
+ color: Color::BLACK,
+ },
+ },
+ print_all_layers: true,
+ paginate_layers: false,
+ shrink_to_fit: EnumMap::from_fn(|_| false),
+ top_continuation: false,
+ bottom_continuation: false,
+ continuation: None,
+ n_orphan_lines: 5,
+ };
+ assert_eq!(&look, &expected);
}
}
Ok(result) => result,
Err(error) => panic!("{error:?}"),
};
- visualization.decode(
- data,
- self.properties
- .as_ref()
- .map_or_else(Look::default, |properties| properties.clone().into()),
- );
+ visualization
+ .decode(
+ data,
+ self.properties
+ .as_ref()
+ .map_or_else(Look::default, |properties| properties.clone().into()),
+ )
+ .unwrap()/*XXX*/;
Ok(PivotTable::new([]).into_item())
}
};
use binrw::{BinRead, BinResult, binread};
+use chrono::{NaiveDateTime, NaiveTime};
use encoding_rs::UTF_8;
use crate::{
+ calendar::{date_time_to_pspp, time_to_pspp},
data::Datum,
- output::spv::light::{U32String, parse_vec},
+ format::{Category, Format},
+ output::{
+ pivot::Value,
+ spv::light::{U32String, decode_format, parse_vec},
+ },
};
#[binread]
pub value: Datum<String>,
}
+impl DataValue {
+ pub fn category(&self) -> Option<usize> {
+ match &self.value {
+ Datum::Number(number) => *number,
+ _ => self.index,
+ }
+ .and_then(|v| (v >= 0.0 && v < usize::MAX as f64).then_some(v as usize))
+ }
+
+ pub fn as_format(&self, format_map: &HashMap<u32, Format>) -> Format {
+ let f = match &self.value {
+ Datum::Number(Some(number)) => *number as u32,
+ Datum::Number(None) => 0,
+ Datum::String(s) => s.parse().unwrap_or_default(),
+ };
+ match format_map.get(&f) {
+ Some(format) => *format,
+ None => decode_format(f),
+ }
+ }
+
+ pub fn as_pivot_value(&self, format: Format) -> Value {
+ if format.type_().category() == Category::Date
+ && let Some(s) = self.value.as_string()
+ && let Ok(date_time) =
+ NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S%.3f")
+ {
+ Value::new_number_with_format(Some(date_time_to_pspp(date_time)), format)
+ } else if format.type_().category() == Category::Time
+ && let Some(s) = self.value.as_string()
+ && let Ok(time) = NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S%.3f")
+ {
+ Value::new_number_with_format(Some(time_to_pspp(time)), format)
+ } else {
+ Value::new_datum_with_format(&self.value, format)
+ }
+ }
+}
+
#[binread]
#[br(little)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
collections::{BTreeMap, HashMap},
marker::PhantomData,
mem::take,
- num::{NonZeroUsize, ParseFloatError},
- str::FromStr,
+ num::NonZeroUsize,
};
use enum_map::{Enum, EnumMap};
-use itertools::Itertools;
use ordered_float::OrderedFloat;
-use serde::{Deserialize, de::Error as _};
+use serde::Deserialize;
use crate::{
data::Datum,
format::{Decimal::Dot, F8_0, Type, UncheckedFormat},
output::{
pivot::{
- self, Area, AreaStyle, Axis2, Axis3, Color, HeadingRegion, HorzAlign, Look, PivotTable,
- RowParity, Value, VertAlign,
+ self, Area, AreaStyle, Axis2, Axis3, Color, HeadingRegion, HorzAlign, Leaf, Length,
+ Look, PivotTable, RowParity, Value, VertAlign,
},
spv::legacy_bin::DataValue,
},
- variable,
};
#[derive(Debug)]
let mut graph = None;
let mut labels = EnumMap::from_fn(|_| Vec::new());
let mut styles = HashMap::new();
- let mut layer_controller = None;
+ let mut _layer_controller = None;
for child in &self.children {
match child {
VisChild::Extension(e) => extension = Some(e),
styles.insert(id.as_str(), style);
}
}
- VisChild::LayerController(lc) => layer_controller = Some(lc),
+ VisChild::LayerController(lc) => _layer_controller = Some(lc),
}
}
let Some(graph) = graph else { todo!() };
- let Some(user_source) = user_source else {
+ let Some(_user_source) = user_source else {
todo!()
};
// Footnotes.
//
- // Any pivot_value might refer to footnotes, so it's important to
- // process the footnotes early to ensure that those references can be
- // resolved. There is a possible problem that a footnote might itself
- // reference an as-yet-unprocessed footnote, but that's OK because
- // footnote references don't actually look at the footnote contents but
- // only resolve a pointer to where the footnote will go later.
+ // Any [Value] might refer to footnotes, so it's important to process
+ // the footnotes early to ensure that those references can be resolved.
+ // There is a possible problem that a footnote might itself reference an
+ // as-yet-unprocessed footnote, but that's OK because footnote
+ // references don't actually look at the footnote contents but only
+ // resolve a pointer to where the footnote will go later.
//
// Before we really start, create all the footnotes we'll fill in. This
// is because sometimes footnotes refer to themselves or to each other
// and we don't want to reject those references.
- let mut footnotes = BTreeMap::<usize, (String, String)>::new();
+ let mut footnote_builder = BTreeMap::<usize, Footnote>::new();
if let Some(f) = &graph.interval.footnotes {
- f.decode(&mut footnotes);
+ f.decode(&mut footnote_builder);
}
for child in &graph.interval.labeling.children {
if let LabelingChild::Footnotes(f) = child {
- f.decode(&mut footnotes);
+ f.decode(&mut footnote_builder);
}
}
for label in &labels[Purpose::Footnote] {
for (index, text) in label.text().iter().enumerate() {
if let Some(uses_reference) = text.uses_reference {
- let entry = footnotes.entry(uses_reference.get() - 1).or_default();
+ let entry = footnote_builder
+ .entry(uses_reference.get() - 1)
+ .or_default();
if index % 2 == 0 {
- entry.0 = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
+ entry.content = text.text.strip_suffix('\n').unwrap_or(&text.text).into();
} else {
- entry.1 = text.text.strip_suffix('.').unwrap_or(&text.text).into();
+ entry.marker =
+ Some(text.text.strip_suffix('.').unwrap_or(&text.text).into());
}
}
}
}
+ let mut footnotes = Vec::new();
+ for (index, footnote) in footnote_builder {
+ while footnotes.len() < index {
+ footnotes.push(pivot::Footnote::default());
+ }
+ footnotes.push(
+ pivot::Footnote::new(footnote.content)
+ .with_marker(footnote.marker.map(|s| Value::new_user_text(s))),
+ );
+ }
+ let footnotes = pivot::Footnotes::from_iter(footnotes);
for (purpose, area) in [
(Purpose::Title, Area::Title),
look.areas[Area::Data(RowParity::Even)].clone();
}
- let mut title = Value::empty();
- let mut caption = Value::empty();
+ let mut _title = Value::empty();
+ let mut _caption = Value::empty();
//Label::decode_
- let show_grid_lines = extension
+ let _show_grid_lines = extension
.as_ref()
.and_then(|extension| extension.show_gridline);
if let Some(style) = styles.get(graph.cell_style.references.as_str())
parts.next();
if let Some(min_width) = parts.next()
&& let Some(max_width) = parts.next()
- && let Ok(min_width) = min_width.parse::<Dimension>()
- && let Ok(max_width) = max_width.parse::<Dimension>()
+ && let Ok(min_width) = min_width.parse::<Length>()
+ && let Ok(max_width) = max_width.parse::<Length>()
{
look.heading_widths[HeadingRegion::Columns] =
- min_width.as_pt() as usize..=max_width.as_pt() as usize;
+ min_width.as_pt_f64() as usize..=max_width.as_pt_f64() as usize;
}
}
styles: &HashMap<&str, &Style>,
a: Axis3,
look: &mut Look,
+ rotate_inner_column_labels: &mut bool,
+ rotate_outer_row_labels: &mut bool,
+ footnotes: &pivot::Footnotes,
) {
let base_level = variables[0].1;
if let Ok(a) = Axis2::try_from(a)
}
if a == Axis3::Y
&& let Some(axis) = axes.get(&(base_level + variables.len() - 1))
- {}
+ {
+ Style::decode(
+ axis.major_ticks.style.get(&styles),
+ axis.major_ticks.tick_frame_style.get(&styles),
+ &mut look.areas[Area::Labels(Axis2::Y)],
+ );
+ }
+
+ if let Some(axis) = axes.get(&base_level)
+ && axis.major_ticks.label_angle == -90.0
+ {
+ if a == Axis3::X {
+ *rotate_inner_column_labels = true;
+ } else {
+ *rotate_outer_row_labels = true;
+ }
+ }
+
+ // Find the first row for each category.
+ let max_cat = variables[0].0.max_category().unwrap()/*XXX*/;
+ let mut cat_rows = vec![None; max_cat + 1];
+ for (index, value) in variables[0].0.values.iter().enumerate() {
+ if let Some(row) = value.category() {
+ cat_rows[row].get_or_insert(index);
+ }
+ }
+
+ // Drop missing categories and count what's left.
+ let cat_rows = cat_rows.into_iter().flatten().collect::<Vec<_>>();
+
+ // Make leaf categories.
+ let mut cats = Vec::with_capacity(cat_rows.len());
+ for row in cat_rows.iter().copied() {
+ let dv = &variables[0].0.values[row];
+ let name = Value::new_datum(&dv.value);
+ let name = variables[0].0.add_affixes(name, &footnotes);
+ cats.push(Leaf::new(name));
+ }
+
+ // Now group them, in one pass per grouping variable, innermost first.
+ for j in 1..variables.len() {
+ // Find a sequence of categories `cat1...cat2`, that all have
+ // the same value in series `j`. (This might be only a single
+ // category.) */
+ let series = variables[j].0;
+ let mut cat1 = 0;
+ while cat1 < cats.len() {
+ let mut cat2 = cat1 + 1;
+ while cat2 < cats.len() {}
+ }
+ }
todo!()
}
styles: &HashMap<&str, &Style>,
a: Axis3,
look: &mut Look,
+ rotate_inner_column_labels: &mut bool,
+ rotate_outer_row_labels: &mut bool,
+ footnotes: &pivot::Footnotes,
+
level_ofs: usize,
) -> Vec<pivot::Dimension> {
let variables = variables
if let Some((var, level)) = var {
dim_vars.push((var, level));
} else if !dim_vars.is_empty() {
- decode_dimension(&dim_vars, axes, styles, a, look);
+ decode_dimension(
+ &dim_vars,
+ axes,
+ styles,
+ a,
+ look,
+ rotate_inner_column_labels,
+ rotate_outer_row_labels,
+ footnotes,
+ );
dim_vars.clear();
}
}
if !dim_vars.is_empty() {
- decode_dimension(&dim_vars, axes, styles, a, look);
+ decode_dimension(
+ &dim_vars,
+ axes,
+ styles,
+ a,
+ look,
+ rotate_inner_column_labels,
+ rotate_outer_row_labels,
+ footnotes,
+ );
}
todo!()
}
+ let mut rotate_inner_column_labels = false;
+ let mut rotate_outer_row_labels = false;
let cross = &graph.faceting.cross.children;
let columns = cross
.first()
.map(|child| child.variables())
.unwrap_or_default();
- decode_dimensions(columns, &series, &axes, &styles, Axis3::X, &mut look, 1);
+ decode_dimensions(
+ columns,
+ &series,
+ &axes,
+ &styles,
+ Axis3::X,
+ &mut look,
+ &mut rotate_inner_column_labels,
+ &mut rotate_outer_row_labels,
+ &footnotes,
+ 1,
+ );
let rows = cross
.get(1)
.map(|child| child.variables())
&styles,
Axis3::Y,
&mut look,
+ &mut rotate_inner_column_labels,
+ &mut rotate_outer_row_labels,
+ &footnotes,
1 + columns.len(),
);
affixes: Vec<Affix>,
}
+impl Series {
+ 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)
+ && let Ok(index) = usize::try_from(index)
+ && let Some(footnote) = footnotes.get(index)
+ {
+ value = value.with_footnote(footnote);
+ }
+ }
+ value
+ }
+
+ fn max_category(&self) -> Option<usize> {
+ self.values
+ .iter()
+ .filter_map(|value| value.category())
+ .max()
+ }
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
enum VisChild {
to: String,
}
-#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd)]
-struct Dimension(f64);
-
-impl Dimension {
- fn as_px(&self) -> f64 {
- self.0 * 96.0
- }
- fn as_pt(&self) -> f64 {
- self.0 * 72.0
- }
-}
-
-impl<'de> Deserialize<'de> for Dimension {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- let string = String::deserialize(deserializer)?;
- Dimension::from_str(&string).map_err(D::Error::custom)
- }
-}
-
-impl FromStr for Dimension {
- type Err = ParseFloatError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- fn parse_unit(s: &str) -> (f64, &str) {
- for (unit, per_inch) in &[
- ("in", 1.0),
- ("인치", 1.0),
- ("pol.", 1.0),
- ("cala", 1.0),
- ("cali", 1.0),
- ("cm", 2.54),
- ("см", 2.54),
- ("pt", 72.0),
- ("пт", 72.0),
- ("px", 96.0),
- ] {
- if let Some(rest) = s.strip_suffix(unit) {
- return (*per_inch, rest);
- }
- }
- (72.0, s)
- }
-
- let (per_inch, s) = parse_unit(s);
- Ok(Self(s.trim().parse::<f64>()? / per_inch))
- }
-}
-
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Style {
font_underline: Option<FontUnderline>,
#[serde(rename = "@margin-bottom")]
- margin_bottom: Option<Dimension>,
+ margin_bottom: Option<Length>,
#[serde(rename = "@margin-top")]
- margin_top: Option<Dimension>,
+ margin_top: Option<Length>,
#[serde(rename = "@margin-left")]
- margin_left: Option<Dimension>,
+ margin_left: Option<Length>,
#[serde(rename = "@margin-right")]
- margin_right: Option<Dimension>,
+ margin_right: Option<Length>,
#[serde(rename = "@textAlignment")]
text_alignment: Option<TextAlignment>,
visible: Option<bool>,
#[serde(rename = "@decimal-offset")]
- decimal_offset: Option<Dimension>,
+ decimal_offset: Option<Length>,
}
impl Style {
}
impl TextAlignment {
- fn as_horz_align(&self, decimal_offset: Option<Dimension>) -> Option<HorzAlign> {
+ fn as_horz_align(&self, decimal_offset: Option<Length>) -> Option<HorzAlign> {
match self {
TextAlignment::Left => Some(HorzAlign::Left),
TextAlignment::Right => Some(HorzAlign::Right),
TextAlignment::Center => Some(HorzAlign::Center),
TextAlignment::Decimal => Some(HorzAlign::Decimal {
- offset: decimal_offset.unwrap_or_default().as_px(),
+ offset: decimal_offset.unwrap_or_default().as_px_f64(),
decimal: Dot,
}),
TextAlignment::Mixed => None,
/// Minimum size.
#[serde(rename = "@min")]
- min: Option<Dimension>,
+ min: Option<Length>,
/// Maximum size.
#[serde(rename = "@max")]
- max: Option<Dimension>,
+ max: Option<Length>,
/// An element to attach to. Required when method is attach or same, not
/// observed otherwise.
format: Option<Format>,
}
+#[derive(Clone, Debug, Default)]
+struct Footnote {
+ content: String,
+ marker: Option<String>,
+}
+
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Footnotes {
}
impl Footnotes {
- fn decode(&self, dst: &mut BTreeMap<usize, (String, String)>) {
+ fn decode(&self, dst: &mut BTreeMap<usize, Footnote>) {
for f in &self.mappings {
- dst.entry(f.defines_reference.get() - 1).or_default().0 = f.to.clone();
+ dst.entry(f.defines_reference.get() - 1)
+ .or_default()
+ .content = f.to.clone();
}
}
}
level: usize,
#[serde(rename = "@gap")]
- gap: Option<Dimension>,
+ gap: Option<Length>,
axis: Axis,
}
label_angle: f64,
#[serde(rename = "@length")]
- length: Dimension,
+ length: Length,
#[serde(rename = "@style")]
style: Ref<Style>,
#[serde(rename = "@target")]
target: Option<Ref<Label>>,
}
-
-#[cfg(test)]
-mod tests {
- use std::str::FromStr;
-
- use crate::output::spv::legacy_xml::Dimension;
-
- #[test]
- fn dimension() {
- for s in [
- "1in",
- "1.0인치",
- "1.0e0 cali",
- "96px",
- "72 ",
- "72 пт",
- " 2.54см",
- ] {
- assert_eq!(Dimension(1.0), Dimension::from_str(s).unwrap());
- }
- }
-}
use crate::{
format::{
- Decimal, Decimals, Epoch, Format, NumberStyle, Settings, Type, UncheckedFormat, Width, CC, F40
+ CC, Decimal, Decimals, Epoch, F40, Format, NumberStyle, Settings, Type, UncheckedFormat,
+ Width,
},
output::pivot::{
- self, parse_bool, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look, PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity, StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign
+ self, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition,
+ FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look,
+ PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity,
+ StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign, parse_bool,
},
settings::Show,
};
}
}
-#[binrw::parser(reader, endian)]
-fn parse_format() -> BinResult<Format> {
- let raw = u32::read_options(reader, endian, ())?;
+pub(super) fn decode_format(raw: u32) -> Format {
if raw == 0 || raw == 0x10000 || raw == 1 {
- return Ok(Format::new(Type::F, 40, 2).unwrap());
+ return Format::new(Type::F, 40, 2).unwrap();
}
let raw_type = (raw >> 16) as u16;
let w = ((raw >> 8) & 0xff) as Width;
let d = raw as Decimals;
- Ok(UncheckedFormat::new(type_, w, d).fix())
+ UncheckedFormat::new(type_, w, d).fix()
+}
+
+#[binrw::parser(reader, endian)]
+fn parse_format() -> BinResult<Format> {
+ Ok(decode_format(u32::read_options(reader, endian, ())?))
}
impl ValueNumber {
footnotes: self
.refs
.iter()
- .flat_map(|index| footnotes.0.get(*index as usize))
+ .flat_map(|index| footnotes.get(*index as usize))
.cloned()
.collect(),
}