1use std::{
17 cell::RefCell,
18 convert::Infallible,
19 fmt,
20 fmt::Write as _,
21 io,
22 sync::{
23 Arc,
24 atomic::{AtomicBool, AtomicUsize, Ordering},
25 },
26};
27
28use crate::{
29 engine::{AggregateRow, Branch, GroupInfo},
30 model::{
31 CellValue, ColumnSet, GridColumns, GridRow, NumberFormat, OwnedValue, display_text_in,
32 number_fraction_digits, push_fixed, push_timestamp,
33 },
34 state::{ColumnAlign, ColumnZone},
35 store::PinnedEdge,
36};
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum ExportScope {
45 Visible,
49 Selection,
52 Expanded,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
88#[non_exhaustive]
89pub enum ExportLine {
90 Data,
92 Caption,
94 Footer,
96 Totals,
98}
99
100impl ExportLine {
101 const ALL: [Self; 4] = [Self::Data, Self::Caption, Self::Footer, Self::Totals];
103
104 const fn bit(self) -> u8 {
106 1 << self as u8
107 }
108}
109
110#[derive(Clone, Copy, PartialEq, Eq, Hash)]
112pub struct ExportLines(u8);
113
114impl ExportLines {
115 pub const EMPTY: Self = Self(0);
117
118 pub const ALL: Self = Self((1 << ExportLine::ALL.len()) - 1);
120
121 pub const fn with(self, line: ExportLine) -> Self {
123 Self(self.0 | line.bit())
124 }
125
126 pub const fn without(self, line: ExportLine) -> Self {
128 Self(self.0 & !line.bit())
129 }
130
131 pub const fn contains(self, line: ExportLine) -> bool {
133 self.0 & line.bit() != 0
134 }
135}
136
137impl Default for ExportLines {
138 fn default() -> Self {
139 Self::ALL
140 }
141}
142
143impl fmt::Debug for ExportLines {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.debug_set()
146 .entries(ExportLine::ALL.iter().filter(|line| self.contains(**line)))
147 .finish()
148 }
149}
150
151pub trait CellExporter<R: GridRow>: Send + Sync + 'static {
157 fn write_cell(&self, cell: &ExportCell<'_, R>, out: &mut String);
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub struct RawCells {
179 decimal_separator: char,
180 precision: NumberPrecision,
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
185pub enum NumberPrecision {
186 #[default]
190 Shown,
191 Full,
194}
195
196impl RawCells {
197 pub const fn new() -> Self {
200 Self {
201 decimal_separator: '.',
202 precision: NumberPrecision::Shown,
203 }
204 }
205
206 pub const fn with_precision(mut self, precision: NumberPrecision) -> Self {
209 self.precision = precision;
210 self
211 }
212
213 pub const fn precision(&self) -> NumberPrecision {
215 self.precision
216 }
217
218 pub const fn with_decimal_separator(mut self, separator: char) -> Self {
222 self.decimal_separator = separator;
223 self
224 }
225
226 pub const fn decimal_separator(&self) -> char {
228 self.decimal_separator
229 }
230
231 fn write_float(self, value: f64, format: NumberFormat, out: &mut String) {
232 if !value.is_finite() {
233 return;
234 }
235 let start = out.len();
236 match self.precision {
237 NumberPrecision::Shown => push_fixed(value, format.decimals(), out),
238 NumberPrecision::Full => {
241 let _ = write!(out, "{value}");
242 }
243 }
244 self.replace_point(start, out);
245 }
246
247 #[cfg(feature = "decimal")]
250 fn write_decimal(self, value: rust_decimal::Decimal, out: &mut String) {
251 let start = out.len();
252 let _ = write!(out, "{value}");
253 self.replace_point(start, out);
254 }
255
256 fn replace_point(self, start: usize, out: &mut String) {
258 if self.decimal_separator != '.'
259 && let Some(dot) = out[start..].find('.')
260 {
261 let dot = start + dot;
262 let mut separator = [0; 4];
263 out.replace_range(
264 dot..dot + 1,
265 self.decimal_separator.encode_utf8(&mut separator),
266 );
267 }
268 }
269}
270
271impl Default for RawCells {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl<R: GridRow> CellExporter<R> for RawCells {
278 #[inline]
279 fn write_cell(&self, cell: &ExportCell<'_, R>, out: &mut String) {
280 match cell.value() {
281 CellValue::Custom(value) => match value.export_value() {
282 Some(export) => self.write_value(export, cell.number_format(), out),
284 None => value.write_text(out),
285 },
286 value => self.write_value(value, cell.number_format(), out),
287 }
288 }
289}
290
291impl RawCells {
292 #[inline]
294 fn write_value(self, value: CellValue<'_>, format: NumberFormat, out: &mut String) {
295 match value {
296 CellValue::Empty => {}
297 CellValue::Bool(value) => out.push_str(if value { "TRUE" } else { "FALSE" }),
298 CellValue::Int(value) => {
299 let _ = write!(out, "{value}");
300 }
301 CellValue::Float(value) => self.write_float(value, format, out),
302 #[cfg(feature = "decimal")]
303 CellValue::Decimal(value) => self.write_decimal(value, out),
304 CellValue::Text(text) => out.push_str(text),
305 CellValue::Timestamp(value) => push_timestamp(value, out),
306 CellValue::Icon(icon) => out.push_str(icon.text()),
307 CellValue::Image(image) => out.push_str(image.text()),
308 CellValue::Link(link) => out.push_str(link.text()),
311 CellValue::Custom(value) => value.write_text(out),
312 }
313 }
314}
315
316#[derive(Clone, Copy, Debug, Default)]
321pub struct DisplayedCells;
322
323thread_local! {
324 static DISPLAY_BUFFER: RefCell<String> = const { RefCell::new(String::new()) };
327}
328
329impl<R: GridRow> CellExporter<R> for DisplayedCells {
330 #[inline]
331 fn write_cell(&self, cell: &ExportCell<'_, R>, out: &mut String) {
332 DISPLAY_BUFFER.with_borrow_mut(|buffer| {
333 out.push_str(display_text_in(cell.value(), cell.number_format(), buffer))
334 });
335 }
336}
337
338#[non_exhaustive]
369pub enum CellExport<R: GridRow> {
370 Raw(RawCells),
372 Displayed(DisplayedCells),
374 Custom(Arc<dyn CellExporter<R>>),
376}
377
378impl<R: GridRow> Default for CellExport<R> {
379 fn default() -> Self {
380 Self::Raw(RawCells::new())
381 }
382}
383
384impl<R: GridRow> Clone for CellExport<R> {
385 fn clone(&self) -> Self {
386 match self {
387 Self::Raw(raw) => Self::Raw(*raw),
388 Self::Displayed(displayed) => Self::Displayed(*displayed),
389 Self::Custom(custom) => Self::Custom(custom.clone()),
390 }
391 }
392}
393
394impl<R: GridRow> CellExporter<R> for CellExport<R> {
395 #[inline]
396 fn write_cell(&self, cell: &ExportCell<'_, R>, out: &mut String) {
397 match self {
398 Self::Raw(raw) => raw.write_cell(cell, out),
399 Self::Displayed(displayed) => displayed.write_cell(cell, out),
400 Self::Custom(custom) => custom.write_cell(cell, out),
401 }
402 }
403}
404
405pub trait Exporter<R: GridRow>: Send + Sync + 'static {
410 fn export(&self, table: &ExportTable<'_, R>, cells: &CellExport<R>, out: &mut String);
412}
413
414pub trait ExportFormat<R: GridRow>: Send + Sync + 'static {
504 fn extension(&self) -> &str;
506
507 fn export(
510 &self,
511 table: &ExportTable<'_, R>,
512 cells: &CellExport<R>,
513 out: &mut dyn io::Write,
514 control: &ExportControl,
515 ) -> Result<(), ExportError>;
516}
517
518#[derive(Clone, Debug)]
521pub struct ExportControl {
522 shared: Arc<ControlState>,
523}
524
525#[derive(Debug)]
526struct ControlState {
527 rows_total: AtomicUsize,
528 rows_written: AtomicUsize,
529 cancelled: AtomicBool,
530}
531
532impl ExportControl {
533 pub fn new(rows_total: usize) -> Self {
536 Self {
537 shared: Arc::new(ControlState {
538 rows_total: AtomicUsize::new(rows_total),
539 rows_written: Default::default(),
540 cancelled: Default::default(),
541 }),
542 }
543 }
544
545 pub fn rows_total(&self) -> usize {
549 self.shared.rows_total.load(Ordering::Relaxed)
550 }
551
552 pub(crate) fn set_rows_total(&self, rows_total: usize) {
554 self.shared.rows_total.store(rows_total, Ordering::Relaxed);
555 }
556
557 pub fn rows_written(&self) -> usize {
559 self.shared.rows_written.load(Ordering::Relaxed)
560 }
561
562 pub fn advance(&self, rows: usize) -> Result<(), ExportError> {
566 self.shared.rows_written.fetch_add(rows, Ordering::Relaxed);
567 if self.is_cancelled() {
568 return Err(ExportError::Cancelled);
569 }
570 Ok(())
571 }
572
573 pub fn is_cancelled(&self) -> bool {
575 self.shared.cancelled.load(Ordering::Relaxed)
576 }
577
578 pub fn cancel(&self) {
580 self.shared.cancelled.store(true, Ordering::Relaxed);
581 }
582}
583
584#[derive(Clone, Debug)]
589#[non_exhaustive]
590pub enum ExportError {
591 Nothing,
594 Cancelled,
596 Io(Arc<io::Error>),
598 Other(Arc<dyn std::error::Error + Send + Sync>),
600}
601
602impl ExportError {
603 pub fn other(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
605 Self::Other(Arc::from(error.into()))
606 }
607}
608
609impl From<io::Error> for ExportError {
610 fn from(error: io::Error) -> Self {
611 Self::Io(Arc::new(error))
612 }
613}
614
615impl fmt::Display for ExportError {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 match self {
618 Self::Nothing => f.write_str("nothing to export"),
619 Self::Cancelled => f.write_str("the export was cancelled"),
620 Self::Io(error) => fmt::Display::fmt(error, f),
621 Self::Other(error) => fmt::Display::fmt(error, f),
622 }
623 }
624}
625
626impl std::error::Error for ExportError {
627 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
629 match self {
630 Self::Io(error) => error.source(),
631 Self::Other(error) => error.source(),
632 Self::Nothing | Self::Cancelled => None,
633 }
634 }
635}
636
637#[derive(Clone, Copy, Debug, Default)]
654pub struct TsvExporter;
655
656const CELL_ESTIMATE: usize = 8;
658
659const FLUSH_ROWS: usize = 1024;
662
663#[derive(Clone, Copy)]
665struct TextDialect {
666 separator: char,
667 between_lines: &'static str,
669 after_line: &'static str,
671}
672
673impl TextDialect {
674 const TSV: Self = Self {
676 separator: '\t',
677 between_lines: "\n",
678 after_line: "",
679 };
680
681 const fn csv(separator: char) -> Self {
683 Self {
684 separator,
685 between_lines: "",
686 after_line: "\r\n",
687 }
688 }
689}
690
691impl<R: GridRow> Exporter<R> for TsvExporter {
692 fn export(&self, table: &ExportTable<'_, R>, cells: &CellExport<R>, out: &mut String) {
693 out.reserve(table.rows().len() * (table.columns().len() * CELL_ESTIMATE + 1));
694 write_delimited(table, cells, TextDialect::TSV, out, |_, _| {
696 Ok::<_, Infallible>(())
697 })
698 .unwrap_or_else(|never| match never {});
699 }
700}
701
702impl<R: GridRow> ExportFormat<R> for TsvExporter {
703 fn extension(&self) -> &str {
704 "tsv"
705 }
706
707 fn export(
708 &self,
709 table: &ExportTable<'_, R>,
710 cells: &CellExport<R>,
711 out: &mut dyn io::Write,
712 control: &ExportControl,
713 ) -> Result<(), ExportError> {
714 write_delimited_file(table, cells, TextDialect::TSV, "", out, control)
715 }
716}
717
718#[derive(Clone, Copy, Debug, PartialEq, Eq)]
749pub struct CsvFormat {
750 separator: char,
751 encoding: CsvEncoding,
752}
753
754impl CsvFormat {
755 pub const fn new() -> Self {
757 Self {
758 separator: ',',
759 encoding: CsvEncoding::Utf8WithBom,
760 }
761 }
762
763 pub const fn with_separator(mut self, separator: char) -> Self {
771 assert!(
772 !matches!(separator, '"' | '\r' | '\n'),
773 "a CSV separator cannot be a quote or a line break"
774 );
775 self.separator = separator;
776 self
777 }
778
779 pub const fn separator(&self) -> char {
781 self.separator
782 }
783
784 pub const fn with_encoding(mut self, encoding: CsvEncoding) -> Self {
786 self.encoding = encoding;
787 self
788 }
789
790 pub const fn encoding(&self) -> CsvEncoding {
792 self.encoding
793 }
794}
795
796impl Default for CsvFormat {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802impl<R: GridRow> ExportFormat<R> for CsvFormat {
803 fn extension(&self) -> &str {
804 "csv"
805 }
806
807 fn export(
808 &self,
809 table: &ExportTable<'_, R>,
810 cells: &CellExport<R>,
811 out: &mut dyn io::Write,
812 control: &ExportControl,
813 ) -> Result<(), ExportError> {
814 write_delimited_file(
815 table,
816 cells,
817 TextDialect::csv(self.separator),
818 self.encoding.preamble(),
819 out,
820 control,
821 )
822 }
823}
824
825#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
827#[non_exhaustive]
828pub enum CsvEncoding {
829 #[default]
832 Utf8WithBom,
833 Utf8,
835}
836
837impl CsvEncoding {
838 const fn preamble(self) -> &'static str {
840 match self {
841 Self::Utf8WithBom => "\u{feff}",
842 Self::Utf8 => "",
843 }
844 }
845}
846
847fn write_delimited<R: GridRow, E>(
852 table: &ExportTable<'_, R>,
853 cells: &CellExport<R>,
854 dialect: TextDialect,
855 buffer: &mut String,
856 mut flush: impl FnMut(&mut String, usize) -> Result<(), E>,
857) -> Result<(), E> {
858 let columns = table.columns();
859 let separator = dialect.separator;
860 let mut scratch = String::new();
863 let mut is_first_line = true;
864 let mut begin_line = |buffer: &mut String| {
865 if !is_first_line {
866 buffer.push_str(dialect.between_lines);
867 }
868 is_first_line = false;
869 };
870
871 if table.has_header() {
872 begin_line(buffer);
873 for (ix, column) in columns.iter().enumerate() {
874 if ix > 0 {
875 buffer.push(separator);
876 }
877 let start = buffer.len();
878 buffer.push_str(column.title());
879 quote_field(buffer, start, separator, &mut scratch);
880 }
881 buffer.push_str(dialect.after_line);
882 }
883 if table.rows().is_empty() {
884 return flush(buffer, 0);
885 }
886 for chunk in table.rows().chunks(FLUSH_ROWS) {
887 for row in chunk {
888 begin_line(buffer);
889 for (ix, column) in columns.iter().enumerate() {
890 if ix > 0 {
891 buffer.push(separator);
892 }
893 if let Some(cell) = row.cell(column.column()) {
894 let start = buffer.len();
895 cells.write_cell(&cell, buffer);
896 quote_field(buffer, start, separator, &mut scratch);
897 }
898 }
899 buffer.push_str(dialect.after_line);
900 }
901 flush(buffer, chunk.len())?;
902 }
903 Ok(())
904}
905
906fn write_delimited_file<R: GridRow>(
909 table: &ExportTable<'_, R>,
910 cells: &CellExport<R>,
911 dialect: TextDialect,
912 preamble: &str,
913 out: &mut dyn io::Write,
914 control: &ExportControl,
915) -> Result<(), ExportError> {
916 let rows = table.rows().len().min(FLUSH_ROWS);
917 let mut buffer = String::with_capacity(
918 preamble.len() + (rows + 1) * (table.columns().len() * CELL_ESTIMATE + 2),
919 );
920 buffer.push_str(preamble);
921 write_delimited(table, cells, dialect, &mut buffer, |buffer, rows| {
922 out.write_all(buffer.as_bytes())?;
923 buffer.clear();
924 control.advance(rows)
925 })?;
926 out.flush()?;
927 Ok(())
928}
929
930#[inline]
934fn quote_field(out: &mut String, start: usize, separator: char, scratch: &mut String) {
935 let field = &out.as_bytes()[start..];
936 let needs_quotes = if separator.is_ascii() {
937 let separator = separator as u8;
938 field
939 .iter()
940 .any(|&byte| byte == separator || matches!(byte, b'\n' | b'\r' | b'"'))
941 } else {
942 field
943 .iter()
944 .any(|byte| matches!(byte, b'\n' | b'\r' | b'"'))
945 || out[start..].contains(separator)
946 };
947 if !needs_quotes {
948 return;
949 }
950 scratch.clear();
951 scratch.push_str(&out[start..]);
952 out.truncate(start);
953 out.push('"');
954 for (ix, part) in scratch.split('"').enumerate() {
955 if ix > 0 {
956 out.push_str("\"\"");
957 }
958 out.push_str(part);
959 }
960 out.push('"');
961}
962
963#[derive(Clone, Debug)]
974pub struct TextFile<E> {
975 exporter: E,
976 extension: Arc<str>,
977}
978
979impl<E> TextFile<E> {
980 pub fn new(exporter: E, extension: impl Into<Arc<str>>) -> Self {
983 Self {
984 exporter,
985 extension: extension.into(),
986 }
987 }
988
989 pub fn exporter(&self) -> &E {
991 &self.exporter
992 }
993}
994
995impl<R: GridRow, E: Exporter<R>> ExportFormat<R> for TextFile<E> {
996 fn extension(&self) -> &str {
997 &self.extension
998 }
999
1000 fn export(
1001 &self,
1002 table: &ExportTable<'_, R>,
1003 cells: &CellExport<R>,
1004 out: &mut dyn io::Write,
1005 control: &ExportControl,
1006 ) -> Result<(), ExportError> {
1007 control.advance(0)?;
1009 let mut text = String::new();
1010 self.exporter.export(table, cells, &mut text);
1011 out.write_all(text.as_bytes())?;
1012 out.flush()?;
1013 control.advance(table.rows().len())
1014 }
1015}
1016
1017#[derive(Clone, Copy, Debug, Default)]
1042pub struct LineFilter<E> {
1043 exporter: E,
1044 lines: ExportLines,
1045}
1046
1047impl<E> LineFilter<E> {
1048 pub fn new(exporter: E, lines: ExportLines) -> Self {
1050 Self { exporter, lines }
1051 }
1052
1053 pub fn exporter(&self) -> &E {
1055 &self.exporter
1056 }
1057
1058 pub fn lines(&self) -> ExportLines {
1060 self.lines
1061 }
1062
1063 fn kept_rows<'a, R: GridRow>(
1065 &self,
1066 table: &ExportTable<'a, R>,
1067 ) -> Option<Vec<ExportRow<'a, R>>> {
1068 let is_kept = |row: &ExportRow<'_, R>| self.lines.contains(row.line());
1069 if table.rows().iter().all(is_kept) {
1070 return None;
1071 }
1072 Some(table.rows().iter().copied().filter(is_kept).collect())
1073 }
1074}
1075
1076impl<R: GridRow, E: Exporter<R>> Exporter<R> for LineFilter<E> {
1077 fn export(&self, table: &ExportTable<'_, R>, cells: &CellExport<R>, out: &mut String) {
1078 match self.kept_rows(table) {
1079 None => self.exporter.export(table, cells, out),
1080 Some(kept) => self.exporter.export(&table.with_rows(&kept), cells, out),
1081 }
1082 }
1083}
1084
1085impl<R: GridRow, E: ExportFormat<R>> ExportFormat<R> for LineFilter<E> {
1086 fn extension(&self) -> &str {
1087 self.exporter.extension()
1088 }
1089
1090 fn export(
1091 &self,
1092 table: &ExportTable<'_, R>,
1093 cells: &CellExport<R>,
1094 out: &mut dyn io::Write,
1095 control: &ExportControl,
1096 ) -> Result<(), ExportError> {
1097 match self.kept_rows(table) {
1098 None => self.exporter.export(table, cells, out, control),
1099 Some(kept) => {
1100 control.advance(table.rows().len() - kept.len())?;
1101 self.exporter
1102 .export(&table.with_rows(&kept), cells, out, control)
1103 }
1104 }
1105 }
1106}
1107
1108#[derive(Clone, Copy, Debug, PartialEq)]
1111pub struct ExportColumn<'a, C> {
1112 column: C,
1113 title: &'a str,
1114 width: Option<f32>,
1115 align: ColumnAlign,
1116 zone: ColumnZone,
1117 bands: &'a [&'a str],
1118}
1119
1120impl<'a, C: GridColumns> ExportColumn<'a, C> {
1121 pub const fn new(column: C, title: &'a str) -> Self {
1124 Self {
1125 column,
1126 title,
1127 width: None,
1128 align: ColumnAlign::Auto,
1129 zone: ColumnZone::Scrolling,
1130 bands: &[],
1131 }
1132 }
1133
1134 pub fn with_width(self, width: f32) -> Self {
1138 Self {
1139 width: Some(width),
1140 ..self
1141 }
1142 }
1143
1144 pub fn with_align(self, align: ColumnAlign) -> Self {
1146 Self { align, ..self }
1147 }
1148
1149 pub fn with_zone(self, zone: ColumnZone) -> Self {
1151 Self { zone, ..self }
1152 }
1153
1154 pub fn with_bands(self, bands: &'a [&'a str]) -> Self {
1157 Self { bands, ..self }
1158 }
1159
1160 pub fn column(&self) -> C {
1162 self.column
1163 }
1164
1165 pub fn title(&self) -> &'a str {
1167 self.title
1168 }
1169
1170 pub fn width(&self) -> Option<f32> {
1172 self.width
1173 }
1174
1175 pub fn align(&self) -> ColumnAlign {
1177 self.align
1178 }
1179
1180 pub fn zone(&self) -> ColumnZone {
1183 self.zone
1184 }
1185
1186 pub fn bands(&self) -> &'a [&'a str] {
1189 self.bands
1190 }
1191}
1192
1193pub struct ExportRow<'a, R: GridRow> {
1216 source: RowSource<'a, R>,
1217 columns: ColumnSet,
1218 formats: &'a [NumberFormat],
1221 depth: u16,
1222 band: Option<PinnedEdge>,
1223 hidden: bool,
1224}
1225
1226enum RowSource<'a, R: GridRow> {
1228 Data {
1231 row: &'a R,
1232 pending: &'a [(R::Column, OwnedValue)],
1234 branch: Option<Branch>,
1235 },
1236 Caption {
1238 group: GroupInfo<'a>,
1239 value_column: R::Column,
1240 },
1241 Footer(GroupInfo<'a>),
1242 Totals {
1244 aggregates: AggregateRow<'a>,
1245 label: Option<(R::Column, &'a str)>,
1246 },
1247}
1248
1249impl<R: GridRow> Clone for RowSource<'_, R> {
1250 fn clone(&self) -> Self {
1251 *self
1252 }
1253}
1254
1255impl<R: GridRow> Copy for RowSource<'_, R> {}
1256
1257impl<'a, R: GridRow> ExportRow<'a, R> {
1258 pub fn new(row: &'a R) -> Self {
1261 Self::of(
1262 RowSource::Data {
1263 row,
1264 pending: &[],
1265 branch: None,
1266 },
1267 0,
1268 )
1269 }
1270
1271 pub fn caption(value_column: R::Column, group: GroupInfo<'a>) -> Self {
1276 Self::of(
1277 RowSource::Caption {
1278 group,
1279 value_column,
1280 },
1281 group.depth(),
1282 )
1283 }
1284
1285 pub fn footer(group: GroupInfo<'a>) -> Self {
1288 Self::of(RowSource::Footer(group), group.depth())
1289 }
1290
1291 pub fn totals(aggregates: AggregateRow<'a>, label: Option<(R::Column, &'a str)>) -> Self {
1295 Self::of(RowSource::Totals { aggregates, label }, 0)
1296 }
1297
1298 fn of(source: RowSource<'a, R>, depth: u16) -> Self {
1299 Self {
1300 source,
1301 columns: ColumnSet::first(R::Column::all().len()),
1302 formats: &[],
1303 depth,
1304 band: None,
1305 hidden: false,
1306 }
1307 }
1308
1309 pub fn with_number_formats(self, formats: &'a [NumberFormat]) -> Self {
1314 Self { formats, ..self }
1315 }
1316
1317 pub fn with_depth(self, depth: u16) -> Self {
1320 Self { depth, ..self }
1321 }
1322
1323 pub fn with_columns(self, columns: ColumnSet) -> Self {
1326 Self { columns, ..self }
1327 }
1328
1329 pub fn with_band(self, edge: PinnedEdge) -> Self {
1333 Self {
1334 band: Some(edge),
1335 ..self
1336 }
1337 }
1338
1339 pub fn with_hidden(self, hidden: bool) -> Self {
1343 Self { hidden, ..self }
1344 }
1345
1346 pub fn with_branch(self, branch: Branch) -> Self {
1349 let source = match self.source {
1350 RowSource::Data { row, pending, .. } => RowSource::Data {
1351 row,
1352 pending,
1353 branch: Some(branch),
1354 },
1355 source => source,
1356 };
1357 Self { source, ..self }
1358 }
1359
1360 pub fn with_pending(self, pending: &'a [(R::Column, OwnedValue)]) -> Self {
1365 let source = match self.source {
1366 RowSource::Data { row, branch, .. } => RowSource::Data {
1367 row,
1368 pending,
1369 branch,
1370 },
1371 source => source,
1372 };
1373 Self { source, ..self }
1374 }
1375
1376 pub fn line(&self) -> ExportLine {
1378 match self.source {
1379 RowSource::Data { .. } => ExportLine::Data,
1380 RowSource::Caption { .. } => ExportLine::Caption,
1381 RowSource::Footer(_) => ExportLine::Footer,
1382 RowSource::Totals { .. } => ExportLine::Totals,
1383 }
1384 }
1385
1386 pub fn row(&self) -> Option<&'a R> {
1388 match self.source {
1389 RowSource::Data { row, .. } => Some(row),
1390 _ => None,
1391 }
1392 }
1393
1394 pub fn band(&self) -> Option<PinnedEdge> {
1396 self.band
1397 }
1398
1399 pub fn is_hidden(&self) -> bool {
1402 self.hidden
1403 }
1404
1405 pub fn branch(&self) -> Option<Branch> {
1408 match self.source {
1409 RowSource::Data { branch, .. } => branch,
1410 _ => None,
1411 }
1412 }
1413
1414 pub fn group(&self) -> Option<GroupInfo<'a>> {
1417 match self.source {
1418 RowSource::Caption { group, .. } | RowSource::Footer(group) => Some(group),
1419 _ => None,
1420 }
1421 }
1422
1423 pub fn depth(&self) -> u16 {
1425 self.depth
1426 }
1427
1428 pub fn columns(&self) -> ColumnSet {
1430 self.columns
1431 }
1432
1433 pub fn cell(&self, column: R::Column) -> Option<ExportCell<'a, R>> {
1436 let ix = column.ix();
1437 if !self.columns.contains(ix) {
1438 return None;
1439 }
1440 let mut is_pending = false;
1441 let (source, value, is_aggregate) = match self.source {
1442 RowSource::Data { row, pending, .. } => {
1443 let value = match pending.iter().find(|(edited, _)| *edited == column) {
1444 Some((_, value)) => {
1445 is_pending = true;
1446 value.as_cell()
1447 }
1448 None => row.value(column),
1449 };
1450 (CellSource::Data(row), value, false)
1451 }
1452 RowSource::Caption {
1453 group,
1454 value_column,
1455 } => {
1456 let source = CellSource::Caption(group);
1457 if column == value_column {
1458 (source, group.value().as_cell(), false)
1459 } else {
1460 (source, group.aggregates().get(ix)?.as_cell(), true)
1461 }
1462 }
1463 RowSource::Footer(group) => (
1464 CellSource::Footer(group),
1465 group.aggregates().get(ix)?.as_cell(),
1466 true,
1467 ),
1468 RowSource::Totals { aggregates, label } => match (aggregates.get(ix), label) {
1469 (Some(aggregate), _) => (CellSource::Totals, aggregate.as_cell(), true),
1470 (None, Some((label_column, label))) if label_column == column => {
1471 (CellSource::Totals, CellValue::Text(label), false)
1472 }
1473 (None, _) => return None,
1474 },
1475 };
1476 Some(ExportCell {
1477 source,
1478 column,
1479 value,
1480 number_format: self.formats.get(ix.get()).copied().unwrap_or_default(),
1481 depth: self.depth,
1482 is_aggregate,
1483 is_pending,
1484 })
1485 }
1486}
1487
1488impl<R: GridRow> Clone for ExportRow<'_, R> {
1489 fn clone(&self) -> Self {
1490 *self
1491 }
1492}
1493
1494impl<R: GridRow> Copy for ExportRow<'_, R> {}
1495
1496pub struct ExportCell<'a, R: GridRow> {
1498 source: CellSource<'a, R>,
1499 column: R::Column,
1500 value: CellValue<'a>,
1501 number_format: NumberFormat,
1502 depth: u16,
1503 is_aggregate: bool,
1504 is_pending: bool,
1505}
1506
1507enum CellSource<'a, R: GridRow> {
1510 Data(&'a R),
1511 Caption(GroupInfo<'a>),
1512 Footer(GroupInfo<'a>),
1513 Totals,
1514}
1515
1516impl<R: GridRow> Clone for CellSource<'_, R> {
1517 fn clone(&self) -> Self {
1518 *self
1519 }
1520}
1521
1522impl<R: GridRow> Copy for CellSource<'_, R> {}
1523
1524impl<'a, R: GridRow> ExportCell<'a, R> {
1525 pub fn row(&self) -> Option<&'a R> {
1528 match self.source {
1529 CellSource::Data(row) => Some(row),
1530 _ => None,
1531 }
1532 }
1533
1534 pub fn column(&self) -> R::Column {
1536 self.column
1537 }
1538
1539 pub fn value(&self) -> CellValue<'a> {
1543 self.value
1544 }
1545
1546 pub fn is_pending(&self) -> bool {
1549 self.is_pending
1550 }
1551
1552 pub fn row_value(&self) -> Option<CellValue<'a>> {
1555 match self.source {
1556 CellSource::Data(row) if self.is_pending => Some(row.value(self.column)),
1557 CellSource::Data(_) => Some(self.value),
1558 _ => None,
1559 }
1560 }
1561
1562 pub fn number_format(&self) -> NumberFormat {
1565 self.number_format
1566 }
1567
1568 pub fn number_hint(&self) -> Option<NumberHint> {
1576 number_fraction_digits(self.value, self.number_format)
1577 .map(|fraction_digits| NumberHint { fraction_digits })
1578 }
1579
1580 pub fn line(&self) -> ExportLine {
1582 match self.source {
1583 CellSource::Data(_) => ExportLine::Data,
1584 CellSource::Caption(_) => ExportLine::Caption,
1585 CellSource::Footer(_) => ExportLine::Footer,
1586 CellSource::Totals => ExportLine::Totals,
1587 }
1588 }
1589
1590 pub fn depth(&self) -> u16 {
1592 self.depth
1593 }
1594
1595 pub fn group(&self) -> Option<GroupInfo<'a>> {
1598 match self.source {
1599 CellSource::Caption(group) | CellSource::Footer(group) => Some(group),
1600 _ => None,
1601 }
1602 }
1603
1604 pub fn is_aggregate(&self) -> bool {
1608 self.is_aggregate
1609 }
1610}
1611
1612impl<R: GridRow> Clone for ExportCell<'_, R> {
1613 fn clone(&self) -> Self {
1614 *self
1615 }
1616}
1617
1618impl<R: GridRow> Copy for ExportCell<'_, R> {}
1619
1620#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1624pub struct NumberHint {
1625 fraction_digits: u32,
1626}
1627
1628impl NumberHint {
1629 pub const fn fraction_digits(&self) -> u32 {
1632 self.fraction_digits
1633 }
1634}
1635
1636pub struct ExportTable<'a, R: GridRow> {
1643 columns: &'a [ExportColumn<'a, R::Column>],
1644 rows: &'a [ExportRow<'a, R>],
1645 header: bool,
1646}
1647
1648impl<'a, R: GridRow> ExportTable<'a, R> {
1649 pub fn new(columns: &'a [ExportColumn<'a, R::Column>], rows: &'a [ExportRow<'a, R>]) -> Self {
1651 Self {
1652 columns,
1653 rows,
1654 header: false,
1655 }
1656 }
1657
1658 pub fn with_header(self, header: bool) -> Self {
1660 Self { header, ..self }
1661 }
1662
1663 pub fn columns(&self) -> &'a [ExportColumn<'a, R::Column>] {
1665 self.columns
1666 }
1667
1668 pub fn has_header(&self) -> bool {
1670 self.header
1671 }
1672
1673 pub fn rows(&self) -> &'a [ExportRow<'a, R>] {
1675 self.rows
1676 }
1677
1678 pub fn frozen_columns(&self) -> usize {
1681 self.columns
1682 .iter()
1683 .take_while(|column| column.zone() == ColumnZone::FrozenLeft)
1684 .count()
1685 }
1686
1687 pub fn band_depth(&self) -> usize {
1690 self.columns
1691 .iter()
1692 .map(|column| column.bands().len())
1693 .max()
1694 .unwrap_or(0)
1695 }
1696
1697 fn with_rows<'b>(&self, rows: &'b [ExportRow<'b, R>]) -> ExportTable<'b, R>
1699 where
1700 'a: 'b,
1701 {
1702 ExportTable {
1703 columns: self.columns,
1704 rows,
1705 header: self.header,
1706 }
1707 }
1708}
1709
1710impl<R: GridRow> Clone for ExportTable<'_, R> {
1711 fn clone(&self) -> Self {
1712 *self
1713 }
1714}
1715
1716impl<R: GridRow> Copy for ExportTable<'_, R> {}
1717
1718#[cfg(test)]
1719mod tests {
1720 use std::{
1721 sync::{
1722 Mutex,
1723 atomic::{AtomicUsize, Ordering},
1724 },
1725 time::Instant,
1726 };
1727
1728 use super::*;
1729 use crate::{
1730 engine::{
1731 Aggregate, EveryGroupFooter, GroupBy, GroupId, Projection, ProjectionEngine as _,
1732 Rebuild, ViewSpec,
1733 },
1734 highlight::HighlightPolicy,
1735 model::{ColumnIx, OwnedValue, display_text},
1736 store::{Ingest, Op, StoreSnapshot},
1737 };
1738
1739 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1740 enum Column {
1741 Symbol,
1742 Price,
1743 Note,
1744 }
1745
1746 impl GridColumns for Column {
1747 fn all() -> &'static [Self] {
1748 &[Self::Symbol, Self::Price, Self::Note]
1749 }
1750
1751 fn id(self) -> &'static str {
1752 match self {
1753 Self::Symbol => "symbol",
1754 Self::Price => "price",
1755 Self::Note => "note",
1756 }
1757 }
1758
1759 fn ix(self) -> ColumnIx {
1760 ColumnIx::new(self as u16)
1761 }
1762 }
1763
1764 struct Row {
1765 key: u32,
1766 cells: [CellValue<'static>; 3],
1767 parent: Option<u32>,
1768 }
1769
1770 impl Row {
1771 fn new(key: u32, cells: [CellValue<'static>; 3]) -> Self {
1772 Self {
1773 key,
1774 cells,
1775 parent: None,
1776 }
1777 }
1778
1779 fn with_parent(self, parent: u32) -> Self {
1780 Self {
1781 parent: Some(parent),
1782 ..self
1783 }
1784 }
1785 }
1786
1787 impl GridRow for Row {
1788 type Key = u32;
1789 type Column = Column;
1790
1791 fn key(&self) -> &u32 {
1792 &self.key
1793 }
1794
1795 fn value(&self, column: Column) -> CellValue<'_> {
1796 self.cells[column.ix().get()]
1797 }
1798
1799 fn parent_key(&self) -> Option<&u32> {
1800 self.parent.as_ref()
1801 }
1802 }
1803
1804 const COLUMNS: [ExportColumn<'static, Column>; 3] = [
1805 ExportColumn::new(Column::Symbol, "Symbol"),
1806 ExportColumn::new(Column::Price, "Price"),
1807 ExportColumn::new(Column::Note, "Note \"short\""),
1808 ];
1809
1810 fn columns(columns: &[Column]) -> ColumnSet {
1811 let mut set = ColumnSet::EMPTY;
1812 for column in columns {
1813 set.insert(column.ix());
1814 }
1815 set
1816 }
1817
1818 fn cell_text(cells: &CellExport<Row>, value: CellValue<'static>) -> String {
1820 let row = Row::new(0, [value, CellValue::Empty, CellValue::Empty]);
1821 let cell = ExportRow::new(&row).cell(Column::Symbol).unwrap();
1822 let mut out = String::new();
1823 cells.write_cell(&cell, &mut out);
1824 out
1825 }
1826
1827 fn tsv(table: &ExportTable<'_, Row>, cells: &CellExport<Row>) -> String {
1828 let mut out = String::new();
1829 Exporter::export(&TsvExporter, table, cells, &mut out);
1830 out
1831 }
1832
1833 #[test]
1834 fn test_tsv_quoting_and_raw_cells() {
1835 let raw = CellExport::Raw(RawCells::new().with_precision(NumberPrecision::Full));
1837 let cases = [
1838 (CellValue::Int(-1234), "-1234"),
1839 (CellValue::Int(i64::MIN), "-9223372036854775808"),
1840 (CellValue::Float(1234.5), "1234.5"),
1841 (CellValue::Float(0.1), "0.1"),
1842 (CellValue::Float(0.1 + 0.2), "0.30000000000000004"),
1843 (CellValue::Float(-0.5), "-0.5"),
1844 (CellValue::Float(2.0), "2"),
1845 (CellValue::Float(1e21), "1000000000000000000000"),
1846 (CellValue::Float(1e-7), "0.0000001"),
1847 (CellValue::Float(f64::NAN), ""),
1848 (CellValue::Float(f64::INFINITY), ""),
1849 (CellValue::Float(f64::NEG_INFINITY), ""),
1850 (CellValue::Bool(true), "TRUE"),
1851 (CellValue::Bool(false), "FALSE"),
1852 (CellValue::Empty, ""),
1853 (CellValue::Text("Сбербанк"), "Сбербанк"),
1854 ];
1855 for (value, expected) in cases {
1856 assert_eq!(cell_text(&raw, value), expected, "{value:?}");
1857 }
1858 let shown = CellExport::default();
1861 for (value, expected) in [
1862 (CellValue::Float(-0.489_999_999_999_994_9), "-0.49"),
1863 (CellValue::Float(1234.5), "1234.50"),
1864 (CellValue::Float(0.1 + 0.2), "0.30"),
1865 (CellValue::Float(2.0), "2.00"),
1866 (CellValue::Float(1e21), "1000000000000000000000.00"),
1867 (CellValue::Float(f64::NAN), ""),
1868 (CellValue::Int(-1234), "-1234"),
1869 ] {
1870 assert_eq!(cell_text(&shown, value), expected, "{value:?}");
1871 }
1872 let row = Row::new(0, [CellValue::Float(-0.489_999_999_999_994_9); 3]);
1873 let formats = [
1874 NumberFormat::new().with_decimals(1),
1875 NumberFormat::new().with_decimals(4),
1876 ];
1877 let exported = ExportRow::new(&row).with_number_formats(&formats);
1878 let written = |cells: &CellExport<Row>, column| {
1879 let mut out = String::new();
1880 cells.write_cell(&exported.cell(column).unwrap(), &mut out);
1881 out
1882 };
1883 assert_eq!(written(&shown, Column::Symbol), "-0.5");
1884 assert_eq!(written(&shown, Column::Price), "-0.4900");
1885 assert_eq!(written(&shown, Column::Note), "-0.49");
1887 assert_eq!(
1888 written(&CellExport::Displayed(DisplayedCells), Column::Symbol),
1889 "-0.5"
1890 );
1891 assert_eq!(written(&raw, Column::Symbol), "-0.4899999999999949");
1892 assert_eq!(
1893 exported.cell(Column::Price).unwrap().number_hint(),
1894 Some(NumberHint { fraction_digits: 4 })
1895 );
1896
1897 let comma = RawCells::new().with_decimal_separator(',');
1900 assert_eq!(comma.decimal_separator(), ',');
1901 let comma = CellExport::Raw(comma);
1902 assert_eq!(cell_text(&comma, CellValue::Float(-1234.5)), "-1234,50");
1903 assert_eq!(
1904 cell_text(&comma, CellValue::Float(1e21)),
1905 "1000000000000000000000,00"
1906 );
1907 assert_eq!(cell_text(&comma, CellValue::Int(-1234)), "-1234");
1908 assert_eq!(cell_text(&comma, CellValue::Text("1.5")), "1.5");
1909 let arabic = CellExport::Raw(RawCells::new().with_decimal_separator('٫'));
1910 assert_eq!(cell_text(&arabic, CellValue::Float(0.25)), "0٫25");
1911 assert_eq!(cell_text(&arabic, CellValue::Float(0.5)), "0٫50");
1912
1913 let rows = [
1915 Row::new(
1916 1,
1917 [
1918 CellValue::Text("AAPL"),
1919 CellValue::Float(187.25),
1920 CellValue::Text("tab\there"),
1921 ],
1922 ),
1923 Row::new(
1924 2,
1925 [
1926 CellValue::Text("say \"hi\""),
1927 CellValue::Int(-3),
1928 CellValue::Text("two\nlines\r"),
1929 ],
1930 ),
1931 Row::new(
1932 3,
1933 [CellValue::Empty, CellValue::Bool(true), CellValue::Empty],
1934 ),
1935 ];
1936 let all: Vec<_> = rows.iter().map(ExportRow::new).collect();
1937 let table = ExportTable::new(&COLUMNS, &all).with_header(true);
1938 assert!(table.has_header());
1939 assert_eq!(
1940 tsv(&table, &raw),
1941 "Symbol\tPrice\t\"Note \"\"short\"\"\"\n\
1942 AAPL\t187.25\t\"tab\there\"\n\
1943 \"say \"\"hi\"\"\"\t-3\t\"two\nlines\r\"\n\
1944 \tTRUE\t"
1945 );
1946
1947 let rectangle = [
1950 ExportRow::new(&rows[0]).with_columns(columns(&[Column::Symbol])),
1951 ExportRow::new(&rows[1]).with_columns(columns(&[Column::Note])),
1952 ];
1953 assert_eq!(rectangle[1].columns(), columns(&[Column::Note]));
1954 assert!(rectangle[1].cell(Column::Symbol).is_none());
1955 let table = ExportTable::new(&COLUMNS, &rectangle);
1956 assert!(!table.has_header());
1957 assert_eq!(tsv(&table, &raw), "AAPL\t\t\n\t\t\"two\nlines\r\"");
1958
1959 let reordered = [
1961 ExportColumn::new(Column::Price, "Price"),
1962 ExportColumn::new(Column::Symbol, "Symbol"),
1963 ];
1964 let table = ExportTable::new(&reordered, &all[..1]);
1965 assert_eq!(tsv(&table, &comma), "187,25\tAAPL");
1966
1967 assert_eq!(tsv(&ExportTable::new(&COLUMNS[..2], &[]), &raw), "");
1970 let header_only = ExportTable::new(&COLUMNS[..2], &[]).with_header(true);
1971 assert_eq!(tsv(&header_only, &raw), "Symbol\tPrice");
1972 let mut out = String::from("x");
1973 Exporter::export(
1974 &TsvExporter,
1975 &ExportTable::new(&COLUMNS[..1], &all[..1]),
1976 &raw,
1977 &mut out,
1978 );
1979 assert_eq!(out, "xAAPL");
1980 }
1981
1982 #[test]
1983 fn test_raw_cells_new_values() {
1984 use chrono::{TimeZone as _, Utc};
1985
1986 use crate::model::{GridValue, IconValue, ImageValue};
1987
1988 #[derive(Debug)]
1990 struct Tenor(i64);
1991
1992 impl GridValue for Tenor {
1993 fn cmp_value(&self, other: &dyn GridValue) -> std::cmp::Ordering {
1994 other
1995 .downcast_ref::<Tenor>()
1996 .map_or(std::cmp::Ordering::Less, |other| self.0.cmp(&other.0))
1997 }
1998
1999 fn write_text(&self, out: &mut String) {
2000 let _ = write!(out, "{}M", self.0 / 30);
2001 }
2002
2003 fn export_value(&self) -> Option<CellValue<'_>> {
2004 Some(CellValue::Float(self.0 as f64 / 360.0))
2005 }
2006 }
2007
2008 #[derive(Debug)]
2010 struct Severity;
2011
2012 impl GridValue for Severity {
2013 fn cmp_value(&self, _: &dyn GridValue) -> std::cmp::Ordering {
2014 std::cmp::Ordering::Equal
2015 }
2016
2017 fn write_text(&self, out: &mut String) {
2018 out.push_str("Critical");
2019 }
2020 }
2021
2022 #[derive(Debug)]
2024 struct Alias;
2025
2026 impl GridValue for Alias {
2027 fn cmp_value(&self, _: &dyn GridValue) -> std::cmp::Ordering {
2028 std::cmp::Ordering::Equal
2029 }
2030
2031 fn write_text(&self, out: &mut String) {
2032 out.push_str("alias");
2033 }
2034
2035 fn export_value(&self) -> Option<CellValue<'_>> {
2036 Some(CellValue::Custom(&Severity))
2037 }
2038 }
2039
2040 static TENOR: Tenor = Tenor(90);
2041 let placed = Utc.with_ymd_and_hms(2026, 11, 4, 12, 34, 56).unwrap();
2042 let raw = CellExport::default();
2043 let comma = CellExport::Raw(RawCells::new().with_decimal_separator(','));
2044 let displayed = CellExport::Displayed(DisplayedCells);
2045 let mut buffer = String::new();
2046 for (value, expected, with_comma) in [
2047 (
2048 CellValue::Timestamp(placed),
2049 "2026-11-04 12:34:56",
2050 "2026-11-04 12:34:56",
2051 ),
2052 (
2053 CellValue::Icon(IconValue::new("icons/check.svg", "Filled")),
2054 "Filled",
2055 "Filled",
2056 ),
2057 (
2058 CellValue::Image(ImageValue::new("images/ada.png", "Ada")),
2059 "Ada",
2060 "Ada",
2061 ),
2062 (CellValue::Custom(&Severity), "Critical", "Critical"),
2063 (CellValue::Custom(&TENOR), "0.25", "0,25"),
2064 (CellValue::Custom(&Alias), "Critical", "Critical"),
2065 ] {
2066 assert_eq!(cell_text(&raw, value), expected, "{value:?}");
2067 assert_eq!(cell_text(&comma, value), with_comma, "{value:?}");
2068 assert_eq!(
2070 cell_text(&displayed, value),
2071 display_text(value, &mut buffer),
2072 "{value:?}"
2073 );
2074 }
2075 assert_eq!(cell_text(&displayed, CellValue::Custom(&TENOR)), "3M");
2076
2077 let rows = [Row::new(
2079 1,
2080 [
2081 CellValue::Icon(IconValue::new("icons/check.svg", "Filled")),
2082 CellValue::Custom(&TENOR),
2083 CellValue::Custom(&Severity),
2084 ],
2085 )];
2086 let all: Vec<_> = rows.iter().map(ExportRow::new).collect();
2087 assert_eq!(
2088 tsv(&ExportTable::new(&COLUMNS, &all), &raw),
2089 "Filled\t0.25\tCritical"
2090 );
2091 assert_eq!(
2092 tsv(&ExportTable::new(&COLUMNS, &all), &displayed),
2093 "Filled\t3M\tCritical"
2094 );
2095 }
2096
2097 #[test]
2098 fn test_cell_export_variants() {
2099 fn assert_send_sync<T: Send + Sync + 'static>() {}
2100 assert_send_sync::<CellExport<Row>>();
2101 assert_send_sync::<TsvExporter>();
2102 assert_send_sync::<Box<dyn Exporter<Row>>>();
2103 assert_send_sync::<Box<dyn ExportFormat<Row>>>();
2104 assert_send_sync::<CsvFormat>();
2105 assert_send_sync::<TextFile<TsvExporter>>();
2106 assert_send_sync::<LineFilter<CsvFormat>>();
2107 assert_send_sync::<ExportControl>();
2108 assert_send_sync::<ExportError>();
2109
2110 assert!(matches!(
2112 CellExport::<Row>::default(),
2113 CellExport::Raw(raw) if raw == RawCells::new() && raw.decimal_separator() == '.'
2114 ));
2115 assert_eq!(RawCells::default(), RawCells::new());
2116
2117 let displayed = CellExport::Displayed(DisplayedCells);
2119 let mut buffer = String::new();
2120 for value in [
2121 CellValue::Empty,
2122 CellValue::Bool(true),
2123 CellValue::Bool(false),
2124 CellValue::Int(-42),
2125 CellValue::Float(1234.567),
2126 CellValue::Float(0.1),
2127 CellValue::Float(f64::NAN),
2128 CellValue::Text("tab\there"),
2129 ] {
2130 assert_eq!(
2131 cell_text(&displayed, value),
2132 display_text(value, &mut buffer),
2133 "{value:?}"
2134 );
2135 }
2136
2137 #[derive(Default)]
2140 struct KeyAndColumn {
2141 calls: AtomicUsize,
2142 }
2143
2144 impl CellExporter<Row> for KeyAndColumn {
2145 fn write_cell(&self, cell: &ExportCell<'_, Row>, out: &mut String) {
2146 self.calls.fetch_add(1, Ordering::Relaxed);
2147 let key = cell.row().expect("a data row").key();
2148 let _ = write!(out, "{key}:{}", cell.column().id());
2149 if cell.value() == CellValue::Empty {
2150 out.push('\t');
2151 }
2152 }
2153 }
2154
2155 let custom = Arc::new(KeyAndColumn::default());
2156 let cells = CellExport::Custom(custom.clone());
2157 let rows = [
2158 Row::new(7, [CellValue::Int(1), CellValue::Empty, CellValue::Int(3)]),
2159 Row::new(9, [CellValue::Int(4); 3]),
2160 ];
2161 let exported = [
2162 ExportRow::new(&rows[0]),
2163 ExportRow::new(&rows[1]).with_columns(columns(&[Column::Price])),
2164 ];
2165 let table = ExportTable::new(&COLUMNS, &exported).with_header(true);
2166 assert_eq!(
2167 tsv(&table, &cells.clone()),
2168 "Symbol\tPrice\t\"Note \"\"short\"\"\"\n\
2169 7:symbol\t\"7:price\t\"\t7:note\n\
2170 \t9:price\t"
2171 );
2172 assert_eq!(custom.calls.load(Ordering::Relaxed), 4);
2173 }
2174
2175 fn snapshot_of(
2177 rows: impl IntoIterator<Item = [CellValue<'static>; 3]>,
2178 ) -> Arc<StoreSnapshot<Row>> {
2179 snapshot_of_rows(
2180 rows.into_iter()
2181 .zip(0..)
2182 .map(|(cells, key)| Row::new(key, cells)),
2183 )
2184 }
2185
2186 fn snapshot_of_rows(rows: impl IntoIterator<Item = Row>) -> Arc<StoreSnapshot<Row>> {
2188 let epoch = Instant::now();
2189 let mut ingest = Ingest::new(HighlightPolicy::default(), epoch);
2190 ingest.apply(
2191 rows.into_iter().map(|row| Op::UpsertSilent(Arc::new(row))),
2192 epoch,
2193 );
2194 ingest.snapshot()
2195 }
2196
2197 fn trades() -> Arc<StoreSnapshot<Row>> {
2201 const TRADES: [(&str, f64, &str); 12] = [
2202 ("AAPL", 1000.25, "buy"),
2203 ("MSFT", 10.0, "x"),
2204 ("SBER", 1.5, "y"),
2205 ("AAPL", 234.25, ""),
2206 ("MSFT", 20.0, "x"),
2207 ("SBER", 1.5, ""),
2208 ("MSFT", 30.0, "x"),
2209 ("SBER", 1.5, "y"),
2210 ("MSFT", 40.0, ""),
2211 ("SBER", 1.5, "y"),
2212 ("MSFT", 50.0, "x"),
2213 ("SBER", 1.5, "y"),
2214 ];
2215 snapshot_of(TRADES.map(|(symbol, price, note)| {
2216 [
2217 CellValue::Text(symbol),
2218 CellValue::Float(price),
2219 if note.is_empty() {
2220 CellValue::Empty
2221 } else {
2222 CellValue::Text(note)
2223 },
2224 ]
2225 }))
2226 }
2227
2228 fn grouped(
2231 snapshot: &StoreSnapshot<Row>,
2232 levels: &[Column],
2233 aggregates: impl IntoIterator<Item = (Column, Aggregate<Row>)>,
2234 ) -> Projection {
2235 let spec = ViewSpec::new()
2236 .with_group_by(levels.iter().map(|&level| GroupBy::column(level)))
2237 .with_aggregates(aggregates)
2238 .with_group_footer(EveryGroupFooter);
2239 Rebuild::default()
2240 .with_max_threads(1)
2241 .rebuild(snapshot, &spec)
2242 }
2243
2244 fn group<'a>(projection: &'a Projection, value: &str) -> GroupInfo<'a> {
2246 projection
2247 .groups()
2248 .iter()
2249 .find(|group| group.depth() == 0 && group.value() == &OwnedValue::Text(value.into()))
2250 .expect("the group")
2251 }
2252
2253 fn aapl_lines<'a>(
2256 snapshot: &'a StoreSnapshot<Row>,
2257 projection: &'a Projection,
2258 ) -> Vec<ExportRow<'a, Row>> {
2259 let aapl = group(projection, "AAPL");
2260 let mut rows = vec![ExportRow::caption(Column::Symbol, aapl)];
2261 rows.extend(
2262 aapl.rows()
2263 .iter()
2264 .map(|&id| ExportRow::new(&**snapshot.row(id).unwrap()).with_depth(1)),
2265 );
2266 rows.push(ExportRow::footer(aapl));
2267 rows.push(ExportRow::totals(
2268 projection.totals(),
2269 Some((Column::Symbol, "Total")),
2270 ));
2271 rows
2272 }
2273
2274 const SUMMARY_COLUMNS: [ExportColumn<'static, Column>; 3] = [
2275 ExportColumn::new(Column::Symbol, "Symbol"),
2276 ExportColumn::new(Column::Price, "Price"),
2277 ExportColumn::new(Column::Note, "Note"),
2278 ];
2279
2280 fn cells_of<'a>(row: &ExportRow<'a, Row>) -> Vec<Option<(CellValue<'a>, bool)>> {
2283 Column::all()
2284 .iter()
2285 .map(|&column| {
2286 row.cell(column)
2287 .map(|cell| (cell.value(), cell.is_aggregate()))
2288 })
2289 .collect()
2290 }
2291
2292 #[test]
2293 fn test_export_summary_cells() {
2294 use CellValue::{Empty, Float, Int, Text};
2295
2296 let snapshot = trades();
2297 let projection = grouped(
2298 &snapshot,
2299 &[Column::Symbol],
2300 [
2301 (Column::Symbol, Aggregate::Count),
2302 (Column::Price, Aggregate::Sum),
2303 (Column::Note, Aggregate::Sum),
2305 ],
2306 );
2307 let aapl = group(&projection, "AAPL");
2308
2309 let caption = ExportRow::caption(Column::Symbol, aapl);
2313 assert_eq!(caption.line(), ExportLine::Caption);
2314 assert!(caption.row().is_none());
2315 assert_eq!(caption.group().map(|group| group.id()), Some(aapl.id()));
2316 assert_eq!(caption.depth(), 0);
2317 assert_eq!(
2318 cells_of(&caption),
2319 [
2320 Some((Text("AAPL"), false)),
2321 Some((Float(1234.5), true)),
2322 Some((Empty, true)),
2323 ]
2324 );
2325 assert_eq!(
2328 cells_of(&ExportRow::caption(Column::Price, aapl)),
2329 [
2330 Some((Int(2), true)),
2331 Some((Text("AAPL"), false)),
2332 Some((Empty, true)),
2333 ]
2334 );
2335 assert_eq!(
2337 cells_of(&caption.with_columns(columns(&[Column::Price]))),
2338 [None, Some((Float(1234.5), true)), None]
2339 );
2340
2341 let footer = ExportRow::footer(aapl);
2343 assert_eq!(footer.line(), ExportLine::Footer);
2344 assert!(footer.row().is_none());
2345 assert_eq!(footer.group().map(|group| group.id()), Some(aapl.id()));
2346 assert_eq!(footer.depth(), 0);
2347 assert_eq!(
2348 cells_of(&footer),
2349 [
2350 Some((Int(2), true)),
2351 Some((Float(1234.5), true)),
2352 Some((Empty, true)),
2353 ]
2354 );
2355
2356 let totals = ExportRow::totals(projection.totals(), Some((Column::Symbol, "Total")));
2358 assert_eq!(totals.line(), ExportLine::Totals);
2359 assert!(totals.row().is_none() && totals.group().is_none());
2360 assert_eq!(
2361 cells_of(&totals),
2362 [
2363 Some((Int(12), true)),
2364 Some((Float(1392.0), true)),
2365 Some((Empty, true)),
2366 ]
2367 );
2368
2369 let summed = grouped(
2372 &snapshot,
2373 &[Column::Symbol],
2374 [(Column::Price, Aggregate::Sum)],
2375 );
2376 let aapl = group(&summed, "AAPL");
2377 assert_eq!(
2378 cells_of(&ExportRow::caption(Column::Symbol, aapl)),
2379 [
2380 Some((Text("AAPL"), false)),
2381 Some((Float(1234.5), true)),
2382 None,
2383 ]
2384 );
2385 assert_eq!(
2386 cells_of(&ExportRow::footer(aapl)),
2387 [None, Some((Float(1234.5), true)), None]
2388 );
2389 for (label, expected) in [
2390 (
2391 Some((Column::Symbol, "Total")),
2392 Some((Text("Total"), false)),
2393 ),
2394 (Some((Column::Price, "Total")), None),
2395 (None, None),
2396 ] {
2397 assert_eq!(
2398 cells_of(&ExportRow::totals(summed.totals(), label)),
2399 [expected, Some((Float(1392.0), true)), None],
2400 "{label:?}"
2401 );
2402 }
2403
2404 let trade = &**snapshot.row(aapl.rows()[0]).unwrap();
2407 let data = ExportRow::new(trade);
2408 assert_eq!(data.line(), ExportLine::Data);
2409 assert!(data.row().is_some_and(|row| std::ptr::eq(row, trade)));
2410 assert!(data.group().is_none());
2411 assert_eq!(data.depth(), 0);
2412 assert_eq!(data.with_depth(1).depth(), 1);
2413 assert_eq!(
2414 cells_of(&data),
2415 [
2416 Some((Text("AAPL"), false)),
2417 Some((Float(1000.25), false)),
2418 Some((Text("buy"), false)),
2419 ]
2420 );
2421
2422 let nested = grouped(&snapshot, &[Column::Symbol, Column::Note], []);
2425 let subgroup = nested
2426 .groups()
2427 .iter()
2428 .find(|group| group.depth() == 1)
2429 .expect("a subgroup");
2430 let caption = ExportRow::<Row>::caption(Column::Note, subgroup);
2431 assert_eq!(caption.depth(), 1);
2432 assert_eq!(caption.cell(Column::Note).map(|cell| cell.depth()), Some(1));
2433 assert_eq!(ExportRow::<Row>::footer(subgroup).depth(), 1);
2434 assert_eq!(caption.with_depth(3).depth(), 3);
2435 }
2436
2437 #[test]
2438 fn test_tsv_with_summaries() {
2439 let snapshot = trades();
2440 let projection = grouped(
2441 &snapshot,
2442 &[Column::Symbol],
2443 [
2444 (Column::Price, Aggregate::Sum),
2445 (Column::Note, Aggregate::Count),
2446 ],
2447 );
2448 let rows = aapl_lines(&snapshot, &projection);
2449 let table = ExportTable::new(&SUMMARY_COLUMNS, &rows).with_header(true);
2450
2451 let raw = CellExport::default();
2454 assert_eq!(
2455 tsv(&table, &raw),
2456 "Symbol\tPrice\tNote\n\
2457 AAPL\t1234.50\t1\n\
2458 AAPL\t1000.25\tbuy\n\
2459 AAPL\t234.25\t\n\
2460 \t1234.50\t1\n\
2461 Total\t1392.00\t9"
2462 );
2463 assert_eq!(
2464 tsv(&table, &CellExport::Displayed(DisplayedCells)),
2465 "Symbol\tPrice\tNote\n\
2466 AAPL\t1234.50\t1\n\
2467 AAPL\t1000.25\tbuy\n\
2468 AAPL\t234.25\t\n\
2469 \t1234.50\t1\n\
2470 Total\t1392.00\t9"
2471 );
2472 let comma = CellExport::Raw(RawCells::new().with_decimal_separator(','));
2474 assert!(tsv(&table, &comma).ends_with("\n\t1234,50\t1\nTotal\t1392,00\t9"));
2475
2476 assert!(
2478 tsv(&ExportTable::new(&SUMMARY_COLUMNS, &rows), &raw)
2479 .starts_with("AAPL\t1234.50\t1\nAAPL\t1000.25\tbuy\n")
2480 );
2481
2482 let snapshot = snapshot_of([
2485 [
2486 CellValue::Text("a\tb"),
2487 CellValue::Int(1),
2488 CellValue::Bool(true),
2489 ],
2490 [
2491 CellValue::Text("a\tb"),
2492 CellValue::Int(2),
2493 CellValue::Bool(false),
2494 ],
2495 ]);
2496 let by_flag = grouped(&snapshot, &[Column::Note, Column::Symbol], []);
2497 let captions: Vec<_> = by_flag
2498 .groups()
2499 .iter()
2500 .map(|group| {
2501 let value_column = if group.depth() == 0 {
2502 Column::Note
2503 } else {
2504 Column::Symbol
2505 };
2506 ExportRow::caption(value_column, group)
2507 })
2508 .collect();
2509 let table = ExportTable::new(&SUMMARY_COLUMNS, &captions);
2510 assert_eq!(
2511 tsv(&table, &raw),
2512 "\t\tFALSE\n\"a\tb\"\t\t\n\t\tTRUE\n\"a\tb\"\t\t"
2513 );
2514 assert_eq!(
2515 tsv(&table, &CellExport::Displayed(DisplayedCells)),
2516 "\t\t\n\"a\tb\"\t\t\n\t\t✓\n\"a\tb\"\t\t"
2517 );
2518 }
2519
2520 #[test]
2521 fn test_line_filter() {
2522 assert_eq!(ExportLines::default(), ExportLines::ALL);
2523 for line in ExportLine::ALL {
2524 assert!(ExportLines::ALL.contains(line));
2525 assert!(!ExportLines::EMPTY.contains(line));
2526 assert_eq!(ExportLines::ALL.without(line).with(line), ExportLines::ALL);
2527 assert!(!ExportLines::ALL.without(line).contains(line));
2528 }
2529 assert_eq!(
2530 format!(
2531 "{:?}",
2532 ExportLines::EMPTY
2533 .with(ExportLine::Totals)
2534 .with(ExportLine::Data)
2535 ),
2536 "{Data, Totals}"
2537 );
2538
2539 let snapshot = trades();
2540 let projection = grouped(
2541 &snapshot,
2542 &[Column::Symbol],
2543 [
2544 (Column::Price, Aggregate::Sum),
2545 (Column::Note, Aggregate::Count),
2546 ],
2547 );
2548 let rows = aapl_lines(&snapshot, &projection);
2549 let table = ExportTable::new(&SUMMARY_COLUMNS, &rows).with_header(true);
2550 let raw = CellExport::default();
2551 let filtered = |lines: ExportLines| {
2552 let filter = LineFilter::new(TsvExporter, lines);
2553 assert_eq!(filter.lines(), lines);
2554 let mut out = String::new();
2555 Exporter::export(&filter, &table, &raw, &mut out);
2556 out
2557 };
2558
2559 assert_eq!(filtered(ExportLines::ALL), tsv(&table, &raw));
2561 assert_eq!(
2564 filtered(
2565 ExportLines::ALL
2566 .without(ExportLine::Footer)
2567 .without(ExportLine::Totals)
2568 ),
2569 "Symbol\tPrice\tNote\nAAPL\t1234.50\t1\nAAPL\t1000.25\tbuy\nAAPL\t234.25\t"
2570 );
2571 assert_eq!(
2572 filtered(ExportLines::EMPTY.with(ExportLine::Data)),
2573 "Symbol\tPrice\tNote\nAAPL\t1000.25\tbuy\nAAPL\t234.25\t"
2574 );
2575 assert_eq!(
2576 filtered(ExportLines::ALL.without(ExportLine::Data)),
2577 "Symbol\tPrice\tNote\nAAPL\t1234.50\t1\n\t1234.50\t1\nTotal\t1392.00\t9"
2578 );
2579 assert_eq!(filtered(ExportLines::EMPTY), "Symbol\tPrice\tNote");
2580
2581 struct Shape;
2583
2584 impl Exporter<Row> for Shape {
2585 fn export(&self, table: &ExportTable<'_, Row>, _: &CellExport<Row>, out: &mut String) {
2586 let lines: Vec<_> = table.rows().iter().map(ExportRow::line).collect();
2587 let _ = write!(
2588 out,
2589 "{} {} {lines:?}",
2590 table.columns().len(),
2591 table.has_header()
2592 );
2593 }
2594 }
2595
2596 let filter = LineFilter::new(Shape, ExportLines::EMPTY.with(ExportLine::Caption));
2597 let mut out = String::new();
2598 Exporter::export(&filter, &table, &raw, &mut out);
2599 assert_eq!(out, "3 true [Caption]");
2600 let _: &Shape = filter.exporter();
2601 }
2602
2603 #[test]
2604 fn test_export_cell_row_is_optional() {
2605 #[derive(Default)]
2609 struct Seen(Mutex<Vec<(Option<u32>, ExportLine, u16, bool, Option<GroupId>)>>);
2610
2611 impl CellExporter<Row> for Seen {
2612 fn write_cell(&self, cell: &ExportCell<'_, Row>, out: &mut String) {
2613 self.0.lock().unwrap().push((
2614 cell.row().map(|row| row.key),
2615 cell.line(),
2616 cell.depth(),
2617 cell.is_aggregate(),
2618 cell.group().map(|group| group.id()),
2619 ));
2620 RawCells::new().write_cell(cell, out);
2621 if cell.line() == ExportLine::Caption
2622 && !cell.is_aggregate()
2623 && let Some(group) = cell.group()
2624 {
2625 let _ = write!(out, " ({})", group.row_count());
2626 }
2627 }
2628 }
2629
2630 let snapshot = trades();
2631 let projection = grouped(
2632 &snapshot,
2633 &[Column::Symbol],
2634 [(Column::Price, Aggregate::Sum)],
2635 );
2636 let rows = aapl_lines(&snapshot, &projection);
2637 let aapl = group(&projection, "AAPL").id();
2638 let seen = Arc::new(Seen::default());
2639 let table = ExportTable::new(&SUMMARY_COLUMNS, &rows);
2640 assert_eq!(
2641 tsv(&table, &CellExport::Custom(seen.clone())),
2642 "AAPL (2)\t1234.50\t\n\
2643 AAPL\t1000.25\tbuy\n\
2644 AAPL\t234.25\t\n\
2645 \t1234.50\t\n\
2646 Total\t1392.00\t"
2647 );
2648 use ExportLine::{Caption, Data, Footer, Totals};
2649 assert_eq!(
2650 *seen.0.lock().unwrap(),
2651 [
2652 (None, Caption, 0, false, Some(aapl)),
2653 (None, Caption, 0, true, Some(aapl)),
2654 (Some(0), Data, 1, false, None),
2655 (Some(0), Data, 1, false, None),
2656 (Some(0), Data, 1, false, None),
2657 (Some(3), Data, 1, false, None),
2658 (Some(3), Data, 1, false, None),
2659 (Some(3), Data, 1, false, None),
2660 (None, Footer, 0, true, Some(aapl)),
2661 (None, Totals, 0, false, None),
2662 (None, Totals, 0, true, None),
2663 ]
2664 );
2665 }
2666
2667 #[test]
2668 fn test_export_scope_variants() {
2669 fn scope_name(scope: ExportScope) -> &'static str {
2672 match scope {
2673 ExportScope::Visible => "Visible",
2674 ExportScope::Selection => "Selection",
2675 ExportScope::Expanded => "Expanded",
2676 }
2677 }
2678 fn line_ix(line: ExportLine) -> usize {
2679 match line {
2680 ExportLine::Data => 0,
2681 ExportLine::Caption => 1,
2682 ExportLine::Footer => 2,
2683 ExportLine::Totals => 3,
2684 }
2685 }
2686
2687 let scopes = [
2688 ExportScope::Visible,
2689 ExportScope::Selection,
2690 ExportScope::Expanded,
2691 ];
2692 for (ix, scope) in scopes.into_iter().enumerate() {
2693 assert_eq!(format!("{scope:?}"), scope_name(scope));
2694 for (other_ix, other) in scopes.into_iter().enumerate() {
2695 assert_eq!(scope == other, ix == other_ix, "{scope:?} {other:?}");
2696 }
2697 }
2698 for (ix, line) in ExportLine::ALL.into_iter().enumerate() {
2700 assert_eq!(line_ix(line), ix);
2701 }
2702 }
2703
2704 #[cfg(feature = "decimal")]
2705 #[test]
2706 fn test_decimal_group_value() {
2707 let price = rust_decimal::Decimal::new(1230, 2);
2708 let snapshot = snapshot_of([[
2709 CellValue::Text("AAPL"),
2710 CellValue::Decimal(price),
2711 CellValue::Empty,
2712 ]]);
2713 let projection = grouped(
2714 &snapshot,
2715 &[Column::Price],
2716 [(Column::Symbol, Aggregate::Count)],
2717 );
2718 let caption = [ExportRow::caption(
2719 Column::Price,
2720 projection.groups().iter().next().expect("a group"),
2721 )];
2722 let table = ExportTable::new(&SUMMARY_COLUMNS, &caption);
2723 assert_eq!(tsv(&table, &CellExport::default()), "1\t12.30\t");
2724 let comma = CellExport::Raw(RawCells::new().with_decimal_separator(','));
2725 assert_eq!(tsv(&table, &comma), "1\t12,30\t");
2726 assert_eq!(
2727 tsv(&table, &CellExport::Displayed(DisplayedCells)),
2728 "1\t12.30\t"
2729 );
2730 }
2731
2732 fn file(
2735 format: &dyn ExportFormat<Row>,
2736 table: &ExportTable<'_, Row>,
2737 cells: &CellExport<Row>,
2738 ) -> Vec<u8> {
2739 let control = ExportControl::new(table.rows().len());
2740 let mut out = Vec::new();
2741 format
2742 .export(table, cells, &mut out, &control)
2743 .expect("the export");
2744 assert_eq!(control.rows_written(), control.rows_total());
2745 out
2746 }
2747
2748 fn file_text(
2750 format: &dyn ExportFormat<Row>,
2751 table: &ExportTable<'_, Row>,
2752 cells: &CellExport<Row>,
2753 ) -> String {
2754 String::from_utf8(file(format, table, cells)).expect("UTF-8")
2755 }
2756
2757 #[test]
2758 fn test_csv_rfc4180() {
2759 use CellValue::{Bool, Empty, Float, Int, Text};
2760
2761 let rows = [
2762 Row::new(1, [Text("a,b"), Float(1234.5), Text("say \"hi\"")]),
2763 Row::new(2, [Text("two\nlines"), Bool(true), Text("cr\rhere")]),
2764 Row::new(3, [Text(" padded "), Empty, Text("semi;colon")]),
2765 Row::new(4, [Text("plain"), Int(-3), Bool(false)]),
2766 ];
2767 let all: Vec<_> = rows.iter().map(ExportRow::new).collect();
2768 let table = ExportTable::new(&COLUMNS, &all).with_header(true);
2769 let raw = CellExport::default();
2770
2771 let format = CsvFormat::new();
2772 assert_eq!(format, CsvFormat::default());
2773 assert_eq!(format.separator(), ',');
2774 assert_eq!(format.encoding(), CsvEncoding::Utf8WithBom);
2775 assert_eq!(CsvEncoding::default(), CsvEncoding::Utf8WithBom);
2776 assert_eq!(ExportFormat::<Row>::extension(&format), "csv");
2777
2778 let expected = "Symbol,Price,\"Note \"\"short\"\"\"\r\n\
2782 \"a,b\",1234.50,\"say \"\"hi\"\"\"\r\n\
2783 \"two\nlines\",TRUE,\"cr\rhere\"\r\n\
2784 \x20padded ,,semi;colon\r\n\
2785 plain,-3,FALSE\r\n";
2786 let with_mark = file(&format, &table, &raw);
2787 assert_eq!(with_mark[..3], [0xEF, 0xBB, 0xBF]);
2788 assert_eq!(with_mark[3..], *expected.as_bytes());
2789 let utf8 = format.with_encoding(CsvEncoding::Utf8);
2790 assert_eq!(utf8.encoding(), CsvEncoding::Utf8);
2791 assert_eq!(file_text(&utf8, &table, &raw), expected);
2792
2793 let semicolon = utf8.with_separator(';');
2796 assert_eq!(semicolon.separator(), ';');
2797 let comma = CellExport::Raw(RawCells::new().with_decimal_separator(','));
2798 assert_eq!(
2799 file_text(&semicolon, &table, &comma),
2800 "Symbol;Price;\"Note \"\"short\"\"\"\r\n\
2801 a,b;1234,50;\"say \"\"hi\"\"\"\r\n\
2802 \"two\nlines\";TRUE;\"cr\rhere\"\r\n\
2803 \x20padded ;;\"semi;colon\"\r\n\
2804 plain;-3;FALSE\r\n"
2805 );
2806
2807 let narrowed = [
2810 ExportRow::new(&rows[0]).with_columns(columns(&[Column::Price])),
2811 ExportRow::new(&rows[3]),
2812 ];
2813 assert_eq!(
2814 file_text(
2815 &utf8,
2816 &ExportTable::new(&COLUMNS, &narrowed),
2817 &CellExport::Displayed(DisplayedCells)
2818 ),
2819 ",1234.50,\r\nplain,-3,\r\n"
2820 );
2821
2822 let header_only = ExportTable::new(&COLUMNS[..2], &[]).with_header(true);
2824 assert_eq!(file_text(&utf8, &header_only, &raw), "Symbol,Price\r\n");
2825 assert_eq!(
2826 file(&format, &ExportTable::new(&COLUMNS, &[]), &raw),
2827 [0xEF, 0xBB, 0xBF]
2828 );
2829
2830 let mut field = String::from("x¦a¦b");
2832 quote_field(&mut field, 3, '¦', &mut String::new());
2833 assert_eq!(field, "x¦\"a¦b\"");
2834 let mut field = String::from("a|b");
2835 quote_field(&mut field, 0, '¦', &mut String::new());
2836 assert_eq!(field, "a|b");
2837 }
2838
2839 #[test]
2840 #[should_panic(expected = "a CSV separator cannot be a quote or a line break")]
2841 fn test_csv_separator_is_not_a_quote() {
2842 let _ = CsvFormat::new().with_separator('"');
2843 }
2844
2845 #[test]
2846 fn test_tsv_file_matches_clipboard() {
2847 use CellValue::{Bool, Empty, Float, Int, Text};
2848
2849 let rows = [
2850 Row::new(1, [Text("AAPL"), Float(187.25), Text("tab\there")]),
2851 Row::new(2, [Text("say \"hi\""), Int(-3), Text("two\nlines\r")]),
2852 Row::new(3, [Empty, Bool(true), Empty]),
2853 ];
2854 let all: Vec<_> = rows.iter().map(ExportRow::new).collect();
2855 let rectangle = [
2856 ExportRow::new(&rows[0]).with_columns(columns(&[Column::Symbol])),
2857 ExportRow::new(&rows[1]).with_columns(columns(&[Column::Note])),
2858 ];
2859 let snapshot = trades();
2860 let projection = grouped(
2861 &snapshot,
2862 &[Column::Symbol],
2863 [
2864 (Column::Price, Aggregate::Sum),
2865 (Column::Note, Aggregate::Count),
2866 ],
2867 );
2868 let summaries = aapl_lines(&snapshot, &projection);
2869 let tables = [
2870 ExportTable::new(&COLUMNS, &all).with_header(true),
2871 ExportTable::new(&COLUMNS, &all),
2872 ExportTable::new(&COLUMNS, &rectangle),
2873 ExportTable::new(&SUMMARY_COLUMNS, &summaries).with_header(true),
2874 ExportTable::new(&SUMMARY_COLUMNS, &summaries),
2875 ExportTable::new(&COLUMNS[..2], &[]).with_header(true),
2876 ExportTable::new(&COLUMNS, &[]),
2877 ];
2878 assert_eq!(ExportFormat::<Row>::extension(&TsvExporter), "tsv");
2879 for cells in [
2880 CellExport::default(),
2881 CellExport::Displayed(DisplayedCells),
2882 CellExport::Raw(RawCells::new().with_decimal_separator(',')),
2883 ] {
2884 for table in &tables {
2885 assert_eq!(
2887 file(&TsvExporter, table, &cells),
2888 tsv(table, &cells).as_bytes()
2889 );
2890 }
2891 }
2892 assert!(file(&TsvExporter, &tables[5], &CellExport::default()) == b"Symbol\tPrice");
2893 }
2894
2895 struct ProgressSink {
2897 control: ExportControl,
2898 bytes: Vec<u8>,
2899 writes: Vec<usize>,
2900 }
2901
2902 impl ProgressSink {
2903 fn new(control: &ExportControl) -> Self {
2904 Self {
2905 control: control.clone(),
2906 bytes: Vec::new(),
2907 writes: Vec::new(),
2908 }
2909 }
2910 }
2911
2912 impl io::Write for ProgressSink {
2913 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
2914 self.writes.push(self.control.rows_written());
2915 self.bytes.extend_from_slice(bytes);
2916 Ok(bytes.len())
2917 }
2918
2919 fn flush(&mut self) -> io::Result<()> {
2920 Ok(())
2921 }
2922 }
2923
2924 #[test]
2925 fn test_formats_report_progress_and_stop() {
2926 let control = ExportControl::new(10);
2928 assert_eq!((control.rows_total(), control.rows_written()), (10, 0));
2929 assert!(control.advance(4).is_ok());
2930 let shared = control.clone();
2931 assert_eq!(shared.rows_written(), 4);
2932 assert!(!control.is_cancelled());
2933 shared.cancel();
2934 assert!(control.is_cancelled());
2935 assert!(matches!(control.advance(0), Err(ExportError::Cancelled)));
2936
2937 let rows: Vec<Row> = (0..3000)
2938 .map(|key| {
2939 Row::new(
2940 key,
2941 [
2942 CellValue::Int(key.into()),
2943 CellValue::Float(f64::from(key) / 4.0),
2944 CellValue::Text("x"),
2945 ],
2946 )
2947 })
2948 .collect();
2949 let lines: Vec<_> = rows.iter().map(ExportRow::new).collect();
2950 let table = ExportTable::new(&COLUMNS, &lines).with_header(true);
2951 let raw = CellExport::default();
2952
2953 let csv = CsvFormat::new();
2955 let formats: [(&dyn ExportFormat<Row>, &str, usize); 2] =
2956 [(&csv, "\r\n", 1025), (&TsvExporter, "\n", 1024)];
2957 for (format, line_break, first_buffer_breaks) in formats {
2958 let name = format.extension();
2959 let control = ExportControl::new(table.rows().len());
2960 let mut sink = ProgressSink::new(&control);
2961 format
2962 .export(&table, &raw, &mut sink, &control)
2963 .expect("the export");
2964 assert_eq!(sink.writes, [0, 1024, 2048], "{name}");
2966 assert_eq!(control.rows_written(), control.rows_total(), "{name}");
2967 let whole = sink.bytes;
2968 assert_eq!(whole, file(format, &table, &raw), "{name}");
2969
2970 let control = ExportControl::new(table.rows().len());
2973 control.cancel();
2974 let mut sink = ProgressSink::new(&control);
2975 assert!(
2976 matches!(
2977 format.export(&table, &raw, &mut sink, &control),
2978 Err(ExportError::Cancelled)
2979 ),
2980 "{name}"
2981 );
2982 assert_eq!(sink.writes, [0], "{name}");
2983 assert!(whole.starts_with(&sink.bytes), "{name}");
2984 let text = String::from_utf8(sink.bytes).expect("UTF-8");
2985 assert_eq!(
2986 text.matches(line_break).count(),
2987 first_buffer_breaks,
2988 "{name}"
2989 );
2990 }
2991
2992 let text_file = TextFile::new(TsvExporter, "tsv");
2995 let control = ExportControl::new(table.rows().len());
2996 let mut sink = ProgressSink::new(&control);
2997 text_file
2998 .export(&table, &raw, &mut sink, &control)
2999 .expect("the export");
3000 assert_eq!(sink.writes, [0]);
3001 assert_eq!(control.rows_written(), 3000);
3002 let control = ExportControl::new(table.rows().len());
3003 control.cancel();
3004 let mut sink = ProgressSink::new(&control);
3005 assert!(matches!(
3006 text_file.export(&table, &raw, &mut sink, &control),
3007 Err(ExportError::Cancelled)
3008 ));
3009 assert!(sink.bytes.is_empty());
3010
3011 struct Full;
3013
3014 impl io::Write for Full {
3015 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
3016 Err(io::Error::new(io::ErrorKind::StorageFull, "disk full"))
3017 }
3018
3019 fn flush(&mut self) -> io::Result<()> {
3020 Ok(())
3021 }
3022 }
3023
3024 let control = ExportControl::new(table.rows().len());
3025 let error = CsvFormat::new()
3026 .export(&table, &raw, &mut Full, &control)
3027 .expect_err("a full disk");
3028 assert!(
3029 matches!(&error, ExportError::Io(error) if error.kind() == io::ErrorKind::StorageFull)
3030 );
3031 assert_eq!(control.rows_written(), 0);
3032 }
3033
3034 #[test]
3035 fn test_text_file_and_line_filter() {
3036 let snapshot = trades();
3037 let projection = grouped(
3038 &snapshot,
3039 &[Column::Symbol],
3040 [
3041 (Column::Price, Aggregate::Sum),
3042 (Column::Note, Aggregate::Count),
3043 ],
3044 );
3045 let rows = aapl_lines(&snapshot, &projection);
3046 let table = ExportTable::new(&SUMMARY_COLUMNS, &rows).with_header(true);
3047 let raw = CellExport::default();
3048
3049 let text_file = TextFile::new(TsvExporter, "tsv");
3051 let _: &TsvExporter = text_file.exporter();
3052 assert_eq!(ExportFormat::<Row>::extension(&text_file), "tsv");
3053 assert_eq!(file(&text_file, &table, &raw), tsv(&table, &raw).as_bytes());
3054 let data_only = TextFile::new(
3055 LineFilter::new(TsvExporter, ExportLines::EMPTY.with(ExportLine::Data)),
3056 String::from("txt"),
3057 );
3058 assert_eq!(ExportFormat::<Row>::extension(&data_only), "txt");
3059 assert_eq!(
3060 file_text(&data_only, &table, &raw),
3061 "Symbol\tPrice\tNote\nAAPL\t1000.25\tbuy\nAAPL\t234.25\t"
3062 );
3063
3064 let utf8 = CsvFormat::new().with_encoding(CsvEncoding::Utf8);
3067 let without_footers = LineFilter::new(utf8, ExportLines::ALL.without(ExportLine::Footer));
3068 assert_eq!(ExportFormat::<Row>::extension(&without_footers), "csv");
3069 assert_eq!(
3070 file_text(&without_footers, &table, &raw),
3071 "Symbol,Price,Note\r\n\
3072 AAPL,1234.50,1\r\n\
3073 AAPL,1000.25,buy\r\n\
3074 AAPL,234.25,\r\n\
3075 Total,1392.00,9\r\n"
3076 );
3077 assert_eq!(
3079 file(&LineFilter::new(utf8, ExportLines::ALL), &table, &raw),
3080 file(&utf8, &table, &raw)
3081 );
3082 assert_eq!(
3083 file_text(&LineFilter::new(utf8, ExportLines::EMPTY), &table, &raw),
3084 "Symbol,Price,Note\r\n"
3085 );
3086 }
3087
3088 #[test]
3089 fn test_export_metadata() {
3090 use crate::engine::TreeSpec;
3091
3092 let price = ExportColumn::new(Column::Price, "Price");
3094 assert_eq!(price.width(), None);
3095 assert_eq!(price.align(), ColumnAlign::Auto);
3096 assert_eq!(price.zone(), ColumnZone::Scrolling);
3097 assert!(price.bands().is_empty());
3098
3099 let bands = ["Market", "Quote"];
3101 let price = price
3102 .with_width(140.0)
3103 .with_align(ColumnAlign::End)
3104 .with_zone(ColumnZone::FrozenLeft)
3105 .with_bands(&bands);
3106 assert_eq!(price.width(), Some(140.0));
3107 assert_eq!(price.align(), ColumnAlign::End);
3108 assert_eq!(price.zone(), ColumnZone::FrozenLeft);
3109 assert_eq!(price.bands(), ["Market", "Quote"]);
3110 assert_eq!((price.column(), price.title()), (Column::Price, "Price"));
3111 assert_ne!(price, ExportColumn::new(Column::Price, "Price"));
3112
3113 let symbol = ExportColumn::new(Column::Symbol, "Symbol").with_zone(ColumnZone::FrozenLeft);
3116 let note = ExportColumn::new(Column::Note, "Note").with_bands(&bands[..1]);
3117 let metadata = [symbol, price, note, symbol];
3118 let table = ExportTable::<Row>::new(&metadata, &[]);
3119 assert_eq!(table.frozen_columns(), 2);
3120 assert_eq!(table.band_depth(), 2);
3121 let right = note.with_zone(ColumnZone::FrozenRight);
3122 for (columns, frozen, depth) in [
3123 (&[note, symbol][..], 0, 1),
3124 (&[symbol, right][..], 1, 1),
3125 (&COLUMNS[..], 0, 0),
3126 (&[][..], 0, 0),
3127 ] {
3128 let table = ExportTable::<Row>::new(columns, &[]);
3129 assert_eq!(table.frozen_columns(), frozen, "{columns:?}");
3130 assert_eq!(table.band_depth(), depth, "{columns:?}");
3131 }
3132
3133 let snapshot = snapshot_of_rows([
3135 Row::new(0, [CellValue::Text("Book"); 3]),
3136 Row::new(1, [CellValue::Text("Desk"); 3]).with_parent(0),
3137 Row::new(2, [CellValue::Text("Trader"); 3]).with_parent(1),
3138 Row::new(3, [CellValue::Text("Trader"); 3]).with_parent(1),
3139 ]);
3140 let tree = Rebuild::default().with_max_threads(1).rebuild(
3141 &snapshot,
3142 &ViewSpec::new().with_tree(TreeSpec::new().with_column(Column::Symbol)),
3143 );
3144 let row = |key: u32| &**snapshot.get(&key).expect("the row");
3145 let branch = |key: u32| tree.branch(snapshot.row_id(&key).expect("the row"));
3146 let desk = branch(1).expect("a branch");
3147 assert_eq!(desk.child_count(), 2);
3148 assert_eq!(branch(2), None);
3149
3150 let data = ExportRow::new(row(1));
3151 assert_eq!(data.band(), None);
3152 assert!(!data.is_hidden());
3153 assert_eq!(data.branch(), None);
3154 let marked = data
3155 .with_band(PinnedEdge::Top)
3156 .with_hidden(true)
3157 .with_branch(desk);
3158 assert_eq!(marked.band(), Some(PinnedEdge::Top));
3159 assert!(marked.is_hidden());
3160 assert_eq!(marked.branch(), Some(desk));
3161 assert!(!marked.with_hidden(false).is_hidden());
3162 let pending = [(Column::Note, OwnedValue::Int(1))];
3164 let rebuilt = marked
3165 .with_depth(1)
3166 .with_columns(columns(&[Column::Note]))
3167 .with_pending(&pending);
3168 assert_eq!(
3169 (rebuilt.band(), rebuilt.is_hidden(), rebuilt.branch()),
3170 (Some(PinnedEdge::Top), true, Some(desk))
3171 );
3172 assert!(
3173 rebuilt
3174 .cell(Column::Note)
3175 .is_some_and(|cell| cell.is_pending())
3176 );
3177
3178 let trades = trades();
3180 let projection = grouped(
3181 &trades,
3182 &[Column::Symbol],
3183 [(Column::Price, Aggregate::Sum)],
3184 );
3185 let totals = ExportRow::<Row>::totals(projection.totals(), None)
3186 .with_band(PinnedEdge::Bottom)
3187 .with_branch(desk);
3188 assert_eq!(
3189 (totals.band(), totals.branch()),
3190 (Some(PinnedEdge::Bottom), None)
3191 );
3192 let footer = ExportRow::<Row>::footer(group(&projection, "AAPL")).with_hidden(true);
3193 assert!(footer.is_hidden() && footer.band().is_none());
3194
3195 #[cfg(target_pointer_width = "64")]
3198 assert!(
3199 size_of::<ExportRow<'_, Row>>() <= 128,
3200 "{} bytes",
3201 size_of::<ExportRow<'_, Row>>()
3202 );
3203 }
3204
3205 #[test]
3206 fn test_number_hint() {
3207 use CellValue::{Bool, Empty, Float, Int, Text, Timestamp};
3208
3209 let digits = |value: CellValue<'static>| {
3210 let row = Row::new(0, [value, Empty, Empty]);
3211 ExportRow::new(&row)
3212 .cell(Column::Symbol)
3213 .unwrap()
3214 .number_hint()
3215 .map(|hint| hint.fraction_digits())
3216 };
3217 assert_eq!(digits(Float(1.0)), Some(2));
3218 assert_eq!(digits(Float(-0.125)), Some(2));
3219 assert_eq!(digits(Int(5)), Some(0));
3220 for value in [
3221 Text("1.5"),
3222 Bool(true),
3223 Empty,
3224 Timestamp(chrono::DateTime::<chrono::Utc>::UNIX_EPOCH),
3225 ] {
3226 assert_eq!(digits(value), None, "{value:?}");
3227 }
3228 #[cfg(feature = "decimal")]
3229 {
3230 use rust_decimal::Decimal;
3231
3232 assert_eq!(digits(CellValue::Decimal(Decimal::new(1230, 2))), Some(2));
3233 assert_eq!(digits(CellValue::Decimal(Decimal::new(5, 0))), Some(0));
3234 }
3235
3236 let snapshot = trades();
3239 let projection = grouped(
3240 &snapshot,
3241 &[Column::Symbol],
3242 [
3243 (Column::Price, Aggregate::Sum),
3244 (Column::Note, Aggregate::Count),
3245 ],
3246 );
3247 let aapl = group(&projection, "AAPL");
3248 let hint = |row: ExportRow<'_, Row>, column| row.cell(column).unwrap().number_hint();
3249 let two = hint(ExportRow::footer(aapl), Column::Price).expect("a hint");
3250 assert_eq!(two.fraction_digits(), 2);
3251 assert_eq!(two, two.clone());
3252 assert_eq!(
3253 hint(ExportRow::footer(aapl), Column::Note).map(|hint| hint.fraction_digits()),
3254 Some(0)
3255 );
3256 assert_eq!(
3257 hint(ExportRow::caption(Column::Symbol, aapl), Column::Symbol),
3258 None
3259 );
3260 assert_eq!(
3261 hint(ExportRow::totals(projection.totals(), None), Column::Price),
3262 Some(two)
3263 );
3264 }
3265
3266 #[test]
3267 fn test_pending_values() {
3268 use CellValue::{Float, Int, Text};
3269
3270 let rows = [Row::new(1, [Text("AAPL"), Float(187.25), Text("buy")])];
3271 let pending = [
3272 (Column::Price, OwnedValue::Float(2.5)),
3273 (Column::Note, OwnedValue::Int(7)),
3274 ];
3275 let row = ExportRow::new(&rows[0]).with_pending(&pending);
3276
3277 let price = row.cell(Column::Price).unwrap();
3279 assert_eq!(price.value(), Float(2.5));
3280 assert!(price.is_pending());
3281 assert_eq!(price.row_value(), Some(Float(187.25)));
3282 let note = row.cell(Column::Note).unwrap();
3284 assert_eq!(
3285 (note.value(), note.row_value()),
3286 (Int(7), Some(Text("buy")))
3287 );
3288 assert_eq!(
3289 note.number_hint().map(|hint| hint.fraction_digits()),
3290 Some(0)
3291 );
3292 let symbol = row.cell(Column::Symbol).unwrap();
3294 assert!(!symbol.is_pending());
3295 assert_eq!(
3296 (symbol.value(), symbol.row_value()),
3297 (Text("AAPL"), Some(Text("AAPL")))
3298 );
3299 let plain = ExportRow::new(&rows[0]).cell(Column::Price).unwrap();
3301 assert!(!plain.is_pending());
3302 assert_eq!(plain.row_value(), Some(Float(187.25)));
3303 assert!(
3305 row.with_columns(columns(&[Column::Price]))
3306 .cell(Column::Price)
3307 .is_some_and(|cell| cell.is_pending())
3308 );
3309
3310 struct Stored;
3313
3314 impl CellExporter<Row> for Stored {
3315 fn write_cell(&self, cell: &ExportCell<'_, Row>, out: &mut String) {
3316 RawCells::new().write_value(
3317 cell.row_value().unwrap_or(cell.value()),
3318 cell.number_format(),
3319 out,
3320 );
3321 }
3322 }
3323
3324 let table = ExportTable::new(&COLUMNS, std::slice::from_ref(&row));
3325 assert_eq!(tsv(&table, &CellExport::default()), "AAPL\t2.50\t7");
3326 assert_eq!(
3327 file_text(
3328 &CsvFormat::new().with_encoding(CsvEncoding::Utf8),
3329 &table,
3330 &CellExport::default()
3331 ),
3332 "AAPL,2.50,7\r\n"
3333 );
3334 assert_eq!(
3335 tsv(&table, &CellExport::Custom(Arc::new(Stored))),
3336 "AAPL\t187.25\tbuy"
3337 );
3338
3339 let snapshot = trades();
3341 let projection = grouped(
3342 &snapshot,
3343 &[Column::Symbol],
3344 [(Column::Price, Aggregate::Sum)],
3345 );
3346 let footer = ExportRow::<Row>::footer(group(&projection, "AAPL")).with_pending(&pending);
3347 let sum = footer.cell(Column::Price).unwrap();
3348 assert_eq!(sum.value(), Float(1234.5));
3349 assert!(!sum.is_pending());
3350 assert_eq!(sum.row_value(), None);
3351 }
3352
3353 #[test]
3354 fn test_export_error() {
3355 use std::error::Error as _;
3356
3357 fn assert_error<T: std::error::Error + Clone + Send + Sync + 'static>() {}
3358 assert_error::<ExportError>();
3359
3360 assert_eq!(ExportError::Nothing.to_string(), "nothing to export");
3361 assert_eq!(
3362 ExportError::Cancelled.to_string(),
3363 "the export was cancelled"
3364 );
3365 assert!(ExportError::Cancelled.source().is_none());
3366
3367 fn write_to_missing_directory() -> Result<(), ExportError> {
3370 Err(io::Error::new(io::ErrorKind::NotFound, "no such directory"))?
3371 }
3372 let error = write_to_missing_directory().expect_err("an error");
3373 assert_eq!(error.to_string(), "no such directory");
3374 let ExportError::Io(io_error) = &error else {
3375 panic!("{error:?}");
3376 };
3377 assert_eq!(io_error.kind(), io::ErrorKind::NotFound);
3378 let ExportError::Io(cloned) = error.clone() else {
3379 panic!("a clone of {error:?}");
3380 };
3381 assert!(Arc::ptr_eq(io_error, &cloned));
3382
3383 let message = ExportError::other("the workbook is too large");
3386 assert!(matches!(message, ExportError::Other(_)));
3387 assert_eq!(message.to_string(), "the workbook is too large");
3388 assert!(message.source().is_none());
3389
3390 #[derive(Debug)]
3391 struct Library(io::Error);
3392
3393 impl fmt::Display for Library {
3394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3395 f.write_str("the library failed")
3396 }
3397 }
3398
3399 impl std::error::Error for Library {
3400 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3401 Some(&self.0)
3402 }
3403 }
3404
3405 let library = ExportError::other(Library(io::Error::other("zip")));
3406 assert_eq!(library.to_string(), "the library failed");
3407 assert_eq!(
3408 library.source().map(ToString::to_string).as_deref(),
3409 Some("zip")
3410 );
3411 assert_eq!(library.clone().to_string(), "the library failed");
3412 }
3413}