+use std::{borrow::Cow, fs::File, io::Write, sync::Arc};
+
+use csv::Writer;
+
+use crate::output::pivot::Coord2;
+
+use super::{driver::Driver, pivot::PivotTable, table::Table, Details, Item, TextType};
+
+struct CsvDriver {
+ file: File,
+
+ /// Number of items written so far.
+ n_items: usize,
+}
+
+impl CsvDriver {
+ pub fn new(file: File) -> Self {
+ Self { file, n_items: 0 }
+ }
+
+ fn start_item(&mut self) {
+ if self.n_items > 0 {
+ write!(&mut self.file, "\n").unwrap();
+ }
+ self.n_items += 1;
+ }
+
+ fn output_table_layer(&mut self, pt: &PivotTable, layer: &[usize]) -> Result<(), csv::Error> {
+ let output = pt.output(layer, true);
+ self.start_item();
+
+ let mut writer = Writer::from_writer(&mut self.file);
+ output_table(&mut writer, pt, output.title.as_ref(), Some("Table"))?;
+ output_table(&mut writer, pt, output.layers.as_ref(), Some("Layer"))?;
+ output_table(&mut writer, pt, Some(&output.body), None)?;
+ output_table(&mut writer, pt, output.caption.as_ref(), Some("Caption"))?;
+ output_table(&mut writer, pt, output.footnotes.as_ref(), Some("Footnote"))?;
+ Ok(())
+ }
+}
+
+fn output_table<W>(
+ writer: &mut Writer<W>,
+ pivot_table: &PivotTable,
+ table: Option<&Table>,
+ leader: Option<&str>,
+) -> Result<(), csv::Error>
+where
+ W: Write,
+{
+ let Some(table) = table else {
+ return Ok(());
+ };
+
+ for y in 0..table.n.y() {
+ for x in 0..table.n.x() {
+ let coord = Coord2::new(x, y);
+ let content = table.get(coord);
+ match &content.inner().value {
+ Some(value) if content.is_top_left(coord) => {
+ let display = value.display(Some(pivot_table));
+ let s = match leader {
+ Some(leader) if x == 0 && y == 0 => format!("{leader}: {display}"),
+ _ => display.to_string(),
+ };
+ writer.write_field(&s)?
+ }
+ _ => writer.write_field("")?,
+ }
+ }
+ writer.write_record(None::<&[u8]>)?;
+ }
+
+ Ok(())
+}
+
+impl Driver for CsvDriver {
+ fn name(&self) -> Cow<'static, str> {
+ Cow::from("csv")
+ }
+
+ fn write(&mut self, item: &Arc<Item>) {
+ // todo: error handling (should not unwrap)
+ match &item.details {
+ Details::Chart | Details::Image | Details::Group(_) => (),
+ Details::Message(diagnostic) => {
+ self.start_item();
+ Writer::from_writer(&mut self.file)
+ .write_record([diagnostic.to_string()])
+ .unwrap();
+ }
+ Details::Table(pivot_table) => {
+ for layer in pivot_table.layers(true) {
+ self.output_table_layer(&*pivot_table, &layer).unwrap();
+ }
+ }
+ Details::PageBreak => {
+ self.start_item();
+ write!(&mut self.file, "\n").unwrap();
+ }
+ Details::Text(text) => match text.type_ {
+ TextType::Syntax | TextType::PageTitle => (),
+ TextType::Title | TextType::Log => {
+ self.start_item();
+ let mut writer = Writer::from_writer(&mut self.file);
+ for line in text.content.display(None).to_string().lines() {
+ writer.write_record([line]).unwrap();
+ }
+ }
+ },
+ }
+ }
+
+ fn flush(&mut self) {
+ self.file.flush();
+ }
+}
use std::{
collections::HashMap,
fmt::{Display, Write},
+ iter::{once, repeat},
ops::{Index, Not, Range},
str::from_utf8,
sync::{Arc, OnceLock, Weak},
}
/// A 2-dimensional `(x,y)` pair.
-#[derive(Copy, Clone, Debug, Default)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Coord2(pub EnumMap<Axis2, usize>);
impl Coord2 {
pub struct Rect2(pub EnumMap<Axis2, Range<usize>>);
impl Rect2 {
- fn new(x_range: Range<usize>, y_range: Range<usize>) -> Self {
+ pub fn new(x_range: Range<usize>, y_range: Range<usize>) -> Self {
Self(enum_map! {
Axis2::X => x_range.clone(),
Axis2::Y => y_range.clone(),
})
}
- fn for_cell(cell: Coord2) -> Self {
+ pub fn for_cell(cell: Coord2) -> Self {
Self::new(cell.x()..cell.x() + 1, cell.y()..cell.y() + 1)
}
- fn for_ranges((a, a_range): (Axis2, Range<usize>), b_range: Range<usize>) -> Self {
+ pub fn for_ranges((a, a_range): (Axis2, Range<usize>), b_range: Range<usize>) -> Self {
let b = !a;
let mut ranges = EnumMap::default();
ranges[a] = a_range;
ranges[b] = b_range;
Self(ranges)
}
+ pub fn top_left(&self) -> Coord2 {
+ use Axis2::*;
+ Coord2::new(self[X].start, self[Y].start)
+ }
}
impl From<EnumMap<Axis2, Range<usize>>> for Rect2 {
weight_format: Format,
- /// Current layer indexes, with axes[PIVOT_AXIS_LAYER].n_dimensions
- /// elements. current_layer[i] is an offset into
- /// axes[PIVOT_AXIS_LAYER].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.
+ /// 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.
current_layer: Vec<usize>,
/// Column and row sizing and page breaks.
}
data_indexes
}
+
+ /// Returns an iterator for the layer axis:
+ ///
+ /// - If `print` is true and `self.look.print_all_layers`, then the iterator
+ /// will visit all values of the layer axis.
+ ///
+ /// - Otherwise, the iterator will just visit `self.current_layer`.
+ pub fn layers(&self, print: bool) -> Box<dyn Iterator<Item = SmallVec<[usize; 4]>> + '_> {
+ if print && self.look.print_all_layers {
+ Box::new(self.axes[Axis3::Z].iter())
+ } else {
+ Box::new(once(SmallVec::from_slice(&self.current_layer)))
+ }
+ }
}
+pub struct Layers {}
+
#[derive(Clone, Debug)]
pub struct Footnote {
index: usize,
}
impl Footnote {
- pub fn display_marker<'a, 'b>(&'a self, table: &'b PivotTable) -> DisplayMarker<'a, 'b> {
+ pub fn display_marker<'a, 'b>(
+ &'a self,
+ table: Option<&'b PivotTable>,
+ ) -> DisplayMarker<'a, 'b> {
DisplayMarker {
footnote: self,
table,
}
}
- pub fn display_content<'a, 'b>(&'a self, table: &'b PivotTable) -> DisplayValue<'a, 'b> {
+ pub fn display_content<'a, 'b>(
+ &'a self,
+ table: Option<&'b PivotTable>,
+ ) -> DisplayValue<'a, 'b> {
self.content.display(table)
}
}
pub struct DisplayMarker<'a, 'b> {
footnote: &'a Footnote,
- table: &'b PivotTable,
+ table: Option<&'b PivotTable>,
+}
+
+impl<'a, 'b> DisplayMarker<'a, 'b> {
+ fn marker_type(&self) -> FootnoteMarkerType {
+ if let Some(table) = self.table {
+ table.look.footnote_marker_type
+ } else {
+ FootnoteMarkerType::default()
+ }
+ }
}
impl<'a, 'b> Display for DisplayMarker<'a, 'b> {
write!(f, "{}", marker.display(self.table).without_suffixes())
} else {
let i = self.footnote.index + 1;
- match self.table.look.footnote_marker_type {
+ match self.marker_type() {
FootnoteMarkerType::Alphabetic => write!(f, "{}", Display26Adic(i)),
FootnoteMarkerType::Numeric => write!(f, "{i}"),
}
pub struct DisplayValue<'a, 'b> {
value: &'a Value,
- table: &'b PivotTable,
+ table: Option<&'b PivotTable>,
/// Whether to show subscripts and footnotes (which follow the body).
show_suffixes: bool,
..
} => interpret_show(
|| Settings::global().show_values,
- self.table.show_values,
+ || self.table.map_or(None, |table| table.show_values),
*show,
label,
),
..
} => interpret_show(
|| Settings::global().show_variables,
- self.table.show_variables,
+ || self.table.map_or(None, |table| table.show_variables),
*show,
label,
),
}
}
+ fn small(&self) -> f64 {
+ self.table.map_or(0.0, |table| table.small)
+ }
+
fn template(
&self,
f: &mut std::fmt::Formatter<'_>,
continue;
};
if let Some(arg) = arg.get(0) {
- write!(f, "{}", arg.display(&self.table))?;
+ write!(f, "{}", arg.display(self.table))?;
}
}
b'[' => {
continue;
};
args_consumed = args_consumed.max(index);
- write!(f, "{}", arg.display(&self.table))?;
+ write!(f, "{}", arg.display(self.table))?;
}
c => write!(f, "{c}")?,
}
fn extract_inner_template(input: &[u8]) -> (&[u8], &[u8]) {
for (index, c) in input.iter().copied().enumerate() {
- if c == b':' && (index == 0 || input[index-1] != b'\\') {
+ if c == b':' && (index == 0 || input[index - 1] != b'\\') {
return input.split_at(index);
}
}
fn interpret_show(
global_show: impl Fn() -> Show,
- table_show: Option<Show>,
+ table_show: impl Fn() -> Option<Show>,
value_show: Option<Show>,
label: &String,
) -> (bool, Option<&String>) {
- match value_show.or(table_show).unwrap_or_else(global_show) {
+ match value_show.or_else(table_show).unwrap_or_else(global_show) {
Show::Value => (true, None),
Show::Label => (false, Some(label)),
Show::Both => (true, Some(label)),
if show_value {
let format = if format.type_() == Type::F
&& *honor_small
- && value.is_some_and(|value| value != 0.0 && value.abs() < self.table.small)
+ && value.is_some_and(|value| value != 0.0 && value.abs() < self.small())
{
UncheckedFormat::new(Type::E, 40, format.d() as u8).fix()
} else {
}
ValueInner::Template { args, local, .. } => self.template(f, &local, args),
+ }?;
+
+ if self.show_suffixes {
+ if let Some(styling) = &self.value.styling {
+ for (subscript, delimiter) in
+ styling.subscripts.iter().zip(once('_').chain(repeat(',')))
+ {
+ write!(f, "{delimiter}{subscript}")?;
+ }
+
+ for footnote_index in styling.footnote_indexes.iter().copied() {
+ if let Some(table) = self.table {
+ if let Some(footnote) = table.footnotes.get(footnote_index) {
+ write!(f, "[{}]", footnote.display_marker(self.table))?;
+ }
+ }
+ }
+ }
}
+
+ Ok(())
}
}
impl Value {
// Returns an object that will format this value, including subscripts and
// superscripts and footnotes. Settings on `table` control whether variable
- // and value labels are included.
- fn display<'a, 'b>(&'a self, table: &'b PivotTable) -> DisplayValue<'a, 'b> {
+ // and value labels are included; if `table` is not provided, then defaults
+ // are used. `table` is also needed to display footnote markers.
+ pub fn display<'a, 'b>(&'a self, table: Option<&'b PivotTable>) -> DisplayValue<'a, 'b> {
DisplayValue {
value: self,
table,