use super::serial::{date_to_serial, time_to_fraction};
use crate::args::ArgSchema;
use crate::function::Function;
use crate::traits::{ArgumentHandle, FunctionContext};
use chrono::NaiveDate;
use formualizer_common::{ExcelError, LiteralValue};
use formualizer_macros::func_caps;
#[derive(Debug)]
pub struct DateValueFn;
impl Function for DateValueFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"DATEVALUE"
}
fn min_args(&self) -> usize {
1
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static ONE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| vec![ArgSchema::any()]);
&ONE[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
let date_text = match args[0].value()?.into_literal() {
LiteralValue::Text(s) => s,
LiteralValue::Error(e) => {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
other => {
return Err(ExcelError::new_value()
.with_message(format!("DATEVALUE expects text, got {other:?}")));
}
};
let formats = [
"%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d", "%B %d, %Y", "%b %d, %Y", "%d-%b-%Y", "%d %B %Y", ];
for fmt in &formats {
if let Ok(date) = NaiveDate::parse_from_str(&date_text, fmt) {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
date_to_serial(&date),
)));
}
}
Err(ExcelError::new_value()
.with_message("DATEVALUE could not parse date text in supported formats"))
}
}
#[derive(Debug)]
pub struct TimeValueFn;
impl Function for TimeValueFn {
func_caps!(PURE);
fn name(&self) -> &'static str {
"TIMEVALUE"
}
fn min_args(&self) -> usize {
1
}
fn arg_schema(&self) -> &'static [ArgSchema] {
use std::sync::LazyLock;
static ONE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| vec![ArgSchema::any()]);
&ONE[..]
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
let time_text = match args[0].value()?.into_literal() {
LiteralValue::Text(s) => s,
LiteralValue::Error(e) => {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
other => {
return Err(ExcelError::new_value()
.with_message(format!("TIMEVALUE expects text, got {other:?}")));
}
};
let formats = [
"%H:%M:%S", "%H:%M", "%I:%M:%S %p", "%I:%M %p", ];
for fmt in &formats {
if let Ok(time) = chrono::NaiveTime::parse_from_str(&time_text, fmt) {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(
time_to_fraction(&time),
)));
}
}
Err(ExcelError::new_value()
.with_message("TIMEVALUE could not parse time text in supported formats"))
}
}
pub fn register_builtins() {
use std::sync::Arc;
crate::function_registry::register_function(Arc::new(DateValueFn));
crate::function_registry::register_function(Arc::new(TimeValueFn));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_workbook::TestWorkbook;
use formualizer_parse::parser::{ASTNode, ASTNodeType};
use std::sync::Arc;
fn lit(v: LiteralValue) -> ASTNode {
ASTNode::new(ASTNodeType::Literal(v), None)
}
#[test]
fn test_datevalue_formats() {
let wb = TestWorkbook::new().with_function(Arc::new(DateValueFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "DATEVALUE").unwrap();
let date_str = lit(LiteralValue::Text("2024-01-15".into()));
let result = f
.dispatch(
&[ArgumentHandle::new(&date_str, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert!(matches!(result, LiteralValue::Number(_)));
let date_str = lit(LiteralValue::Text("01/15/2024".into()));
let result = f
.dispatch(
&[ArgumentHandle::new(&date_str, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
assert!(matches!(result, LiteralValue::Number(_)));
}
#[test]
fn test_timevalue_formats() {
let wb = TestWorkbook::new().with_function(Arc::new(TimeValueFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "TIMEVALUE").unwrap();
let time_str = lit(LiteralValue::Text("14:30:00".into()));
let result = f
.dispatch(
&[ArgumentHandle::new(&time_str, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
match result {
LiteralValue::Number(n) => {
assert!((n - 0.6041666667).abs() < 1e-9);
}
_ => panic!("TIMEVALUE should return a number"),
}
let time_str = lit(LiteralValue::Text("02:30 PM".into()));
let result = f
.dispatch(
&[ArgumentHandle::new(&time_str, &ctx)],
&ctx.function_context(None),
)
.unwrap()
.into_literal();
match result {
LiteralValue::Number(n) => {
assert!((n - 0.6041666667).abs() < 1e-9);
}
_ => panic!("TIMEVALUE should return a number"),
}
}
}