use crate::display::display_number;
use crate::eval::functions::{check_arity, FunctionMeta, Registry};
use crate::types::{ErrorKind, SparklineChartType, SparklineSpec, SparklineValue, Value};
const CHART_TYPE_KEY: &str = "charttype";
fn to_sparkline_value(v: &Value) -> Result<SparklineValue, Value> {
match v {
Value::Number(n) | Value::Date(n) => Ok(SparklineValue::number(*n)),
Value::Text(s) => Ok(SparklineValue::Text(s.clone())),
Value::Bool(b) => Ok(SparklineValue::Bool(*b)),
Value::Empty => Ok(SparklineValue::Blank),
Value::Error(_) | Value::ErrorMsg(_, _) => Err(v.clone()),
Value::Array(_) | Value::Zoned(_) | Value::Sparkline(_) => {
Err(Value::Error(ErrorKind::Value))
}
}
}
fn flatten<'a>(v: &'a Value, out: &mut Vec<&'a Value>) {
match v {
Value::Array(elems) => elems.iter().for_each(|e| flatten(e, out)),
other => out.push(other),
}
}
fn parse_data(v: &Value) -> Result<Vec<SparklineValue>, Value> {
if !matches!(v, Value::Array(_)) {
return Err(Value::Error(ErrorKind::NA));
}
let mut cells = Vec::new();
flatten(v, &mut cells);
match cells.len() {
0 => return Err(Value::Error(ErrorKind::Ref)),
1 => return Err(Value::Error(ErrorKind::NA)),
_ => {}
}
cells.iter().map(|c| to_sparkline_value(c)).collect()
}
fn option_key(v: &Value) -> Result<String, Value> {
let key = match v {
Value::Text(s) => s.clone(),
Value::Number(n) | Value::Date(n) => display_number(*n),
Value::Bool(b) => (if *b { "TRUE" } else { "FALSE" }).to_string(),
Value::Empty => String::new(),
Value::Error(_) | Value::ErrorMsg(_, _) => return Err(v.clone()),
Value::Array(_) | Value::Zoned(_) | Value::Sparkline(_) => {
return Err(Value::Error(ErrorKind::Ref))
}
};
Ok(key.to_ascii_lowercase())
}
fn option_pairs(v: &Value) -> Result<Vec<(&Value, &Value)>, Value> {
let Value::Array(elems) = v else {
return Err(Value::Error(ErrorKind::Ref));
};
if elems.is_empty() {
return Err(Value::Error(ErrorKind::Ref));
}
let rows = elems.iter().filter(|e| matches!(e, Value::Array(_))).count();
if rows == elems.len() {
let mut pairs = Vec::with_capacity(elems.len());
for row in elems {
let Value::Array(cells) = row else { unreachable!() };
if cells.len() != 2 {
return Err(Value::Error(ErrorKind::Ref));
}
pairs.push((&cells[0], &cells[1]));
}
Ok(pairs)
} else if rows == 0 {
if elems.len() % 2 != 0 {
return Err(Value::Error(ErrorKind::Ref));
}
Ok(elems.chunks(2).map(|p| (&p[0], &p[1])).collect())
} else {
Err(Value::Error(ErrorKind::Ref))
}
}
pub fn sparkline_fn(args: &[Value]) -> Value {
if let Some(err) = check_arity(args, 1, 2) {
return err;
}
let data = match parse_data(&args[0]) {
Ok(data) => data,
Err(e) => return e,
};
let mut chart_type = SparklineChartType::Line;
let mut options = Vec::new();
if let Some(raw_options) = args.get(1) {
let pairs = match option_pairs(raw_options) {
Ok(pairs) => pairs,
Err(e) => return e,
};
for (raw_key, raw_value) in pairs {
let key = match option_key(raw_key) {
Ok(key) => key,
Err(e) => return e,
};
let value = match to_sparkline_value(raw_value) {
Ok(value) => value,
Err(e) => return e,
};
if key == CHART_TYPE_KEY {
let SparklineValue::Text(ref name) = value else {
return Value::Error(ErrorKind::Value);
};
match SparklineChartType::parse(name) {
Some(t) => chart_type = t,
None => return Value::Error(ErrorKind::Value),
}
} else {
options.push((key, value));
}
}
}
Value::Sparkline(Box::new(SparklineSpec {
chart_type,
data,
options,
}))
}
pub fn register_google(registry: &mut Registry) {
registry.register_eager(
"SPARKLINE",
sparkline_fn,
FunctionMeta {
category: "google",
signature: "SPARKLINE(data, [options])",
description: "Miniature in-cell chart over a range, as a render spec",
},
);
}