use std::collections::HashMap;
use crate::args::Fmt;
use crate::input::Table;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Channel(usize);
impl Channel {
pub fn index(self) -> usize {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Series {
pub x: Option<Channel>,
pub y: Channel,
pub label: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Dataset {
channels: Vec<Vec<f64>>,
pub series: Vec<Series>,
pub unparsed: usize,
}
impl Dataset {
pub fn channels(&self) -> &[Vec<f64>] {
&self.channels
}
pub fn channel(&self, channel: Channel) -> &[f64] {
&self.channels[channel.index()]
}
pub fn x(&self, series: &Series) -> Option<&[f64]> {
series.x.map(|channel| self.channel(channel))
}
pub fn y(&self, series: &Series) -> &[f64] {
self.channel(series.y)
}
}
pub fn default_fmt(columns: usize) -> Fmt {
if columns <= 1 { Fmt::Y } else { Fmt::Xyy }
}
fn parse_number(field: &str) -> Option<f64> {
let trimmed = field.trim();
if trimmed.is_empty() {
return None;
}
trimmed
.parse::<f64>()
.ok()
.filter(|value| value.is_finite())
}
fn build_column(table: &Table, index: usize, time: bool) -> (Vec<f64>, usize) {
let mut out = Vec::with_capacity(table.rows.len());
let mut unparsed = 0;
for row in &table.rows {
match row.get(index) {
Some(field) => {
let parsed = if time {
crate::time::parse(field)
} else {
parse_number(field)
};
match parsed {
Some(value) => out.push(value),
None => {
unparsed += 1;
out.push(f64::NAN);
}
}
}
None => out.push(f64::NAN),
}
}
(out, unparsed)
}
fn numeric_columns(table: &Table) -> (Vec<Vec<f64>>, usize) {
let mut columns = Vec::with_capacity(table.width());
let mut unparsed = 0;
for index in 0..table.width() {
let (column, count) = build_column(table, index, false);
unparsed += count;
columns.push(column);
}
(columns, unparsed)
}
fn label(table: &Table, index: usize) -> Option<String> {
table
.header
.as_ref()
.and_then(|names| names.get(index))
.cloned()
}
struct Spec {
x: Option<usize>,
y: usize,
label: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParseAs {
Number,
Time,
}
fn specs(fmt: Fmt, width: usize) -> Vec<Spec> {
match fmt {
Fmt::Y => (0..width)
.map(|i| Spec {
x: None,
y: i,
label: i,
})
.collect(),
Fmt::Xy if width >= 2 => vec![Spec {
x: Some(0),
y: 1,
label: 1,
}],
Fmt::Xyy if width >= 2 => (1..width)
.map(|i| Spec {
x: Some(0),
y: i,
label: i,
})
.collect(),
Fmt::Xyxy => (0..width / 2)
.map(|pair| Spec {
x: Some(pair * 2),
y: pair * 2 + 1,
label: pair * 2 + 1,
})
.collect(),
Fmt::Yx if width >= 2 => vec![Spec {
x: Some(1),
y: 0,
label: 0,
}],
_ => Vec::new(),
}
}
pub fn dataset(table: &Table, fmt: Fmt, time_x: bool) -> Dataset {
let specs = specs(fmt, table.width());
let mut roles = vec![None; table.width()];
for spec in &specs {
insert_role(&mut roles, spec.y, ParseAs::Number);
if let Some(x) = spec.x {
let role = if time_x {
ParseAs::Time
} else {
ParseAs::Number
};
insert_role(&mut roles, x, role);
}
}
let mut channels = Vec::with_capacity(roles.iter().flatten().count());
let mut channel_for = vec![None; table.width()];
let mut unparsed = 0;
for (index, role) in roles.into_iter().enumerate() {
if let Some(role) = role {
let (column, count) = build_column(table, index, role == ParseAs::Time);
channel_for[index] = Some(Channel(channels.len()));
channels.push(column);
unparsed += count;
}
}
let series = specs
.iter()
.map(|spec| Series {
x: spec.x.map(|index| {
channel_for[index].expect("every x role was assigned a parsed channel")
}),
y: channel_for[spec.y].expect("every y role was assigned a parsed channel"),
label: label(table, spec.label),
})
.collect();
Dataset {
channels,
series,
unparsed,
}
}
fn insert_role(roles: &mut [Option<ParseAs>], index: usize, role: ParseAs) {
match roles[index] {
Some(existing) => debug_assert_eq!(existing, role),
None => roles[index] = Some(role),
}
}
pub fn resolve_fmt(table: &Table, requested: Option<Fmt>) -> Fmt {
requested.unwrap_or_else(|| default_fmt(table.width()))
}
pub fn flatten(table: &Table) -> (Vec<f64>, usize) {
let mut values = Vec::new();
let mut unparsed = 0;
for row in &table.rows {
for field in row {
match parse_number(field) {
Some(value) => values.push(value),
None => unparsed += 1,
}
}
}
(values, unparsed)
}
pub fn groups(table: &Table) -> (Vec<String>, Vec<Vec<f64>>, usize) {
let (columns, unparsed) = numeric_columns(table);
let categories = (0..columns.len())
.map(|index| label(table, index).unwrap_or_else(|| (index + 1).to_string()))
.collect();
let groups = columns
.into_iter()
.map(|column| {
column
.into_iter()
.filter(|value| value.is_finite())
.collect()
})
.collect();
(categories, groups, unparsed)
}
pub fn xy(table: &Table, time_x: bool) -> (Vec<f64>, Vec<f64>, usize) {
let (x, ux) = build_column(table, 0, time_x);
let (y, uy) = build_column(table, 1, false);
(x, y, ux + uy)
}
pub fn matrix(table: &Table, flip: bool) -> (usize, Vec<f64>, usize) {
let (columns, unparsed) = numeric_columns(table);
let cols = columns.len();
let rows = table.rows.len();
let mut values = Vec::with_capacity(cols * rows);
for heat_row in 0..rows {
let source = if flip { rows - 1 - heat_row } else { heat_row };
for column in &columns {
values.push(column[source]);
}
}
(cols, values, unparsed)
}
pub fn labeled_values(table: &Table) -> (Vec<String>, Vec<f64>, usize) {
let mut labels = Vec::with_capacity(table.rows.len());
let mut values = Vec::with_capacity(table.rows.len());
let mut unparsed = 0;
for row in &table.rows {
let Some(name) = row.first() else { continue };
labels.push(name.clone());
match row.get(1) {
Some(field) => match parse_number(field) {
Some(value) => values.push(value),
None => {
unparsed += 1;
values.push(f64::NAN);
}
},
None => values.push(f64::NAN),
}
}
(labels, values, unparsed)
}
pub fn counts(table: &Table) -> Vec<(String, f64)> {
let mut order: Vec<String> = Vec::new();
let mut tally: HashMap<&str, u64> = HashMap::new();
for row in &table.rows {
let Some(field) = row.first() else { continue };
let entry = tally.entry(field.as_str()).or_insert(0);
if *entry == 0 {
order.push(field.clone());
}
*entry += 1;
}
let mut counts: Vec<(String, f64)> = order
.into_iter()
.map(|name| {
let count = tally[name.as_str()];
(name, count as f64)
})
.collect();
counts.sort_by(|a, b| {
(b.1)
.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
counts
}
#[cfg(test)]
#[path = "tests/series_tests.rs"]
mod tests;