Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel
4b0dbc598f update: add leftbar to app 2025-09-30 19:46:57 +02:00
210 changed files with 4659 additions and 14079 deletions

30
Cargo.lock generated
View File

@@ -43,15 +43,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -423,7 +414,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -439,7 +430,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -452,12 +443,11 @@ dependencies = [
"ryu", "ryu",
"serde", "serde",
"serde_json", "serde_json",
"statrs",
] ]
[[package]] [[package]]
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"napi", "napi",
@@ -792,7 +782,7 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.6.0" version = "0.5.7"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"ironcalc", "ironcalc",
@@ -975,16 +965,6 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "statrs"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
dependencies = [
"approx",
"num-traits",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"
@@ -1095,7 +1075,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.6.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",

View File

@@ -1,93 +0,0 @@
FROM rust:latest AS builder
WORKDIR /app
COPY . .
# Tools + wasm toolchain + Node via nvm
RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl ca-certificates make \
&& rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& bash -lc "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash -" \
&& bash -lc '\
export NVM_DIR="$HOME/.nvm" && \
source "$NVM_DIR/nvm.sh" && \
nvm install 22 && nvm alias default 22 && \
nroot="$NVM_DIR/versions/node/$(nvm version default)/bin" && \
ln -sf "$nroot/node" /usr/local/bin/node && \
ln -sf "$nroot/npm" /usr/local/bin/npm && \
ln -sf "$nroot/npx" /usr/local/bin/npx \
' \
&& npm install typescript \
&& rm -rf /var/lib/apt/lists/*
# build the server
RUN cargo build --release --manifest-path webapp/app.ironcalc.com/server/Cargo.toml
# build the wasm
RUN make -C bindings/wasm
# build the widget
WORKDIR /app/webapp/IronCalc
RUN npm install && npm run build
# build the frontend app
WORKDIR /app/webapp/app.ironcalc.com/frontend
RUN npm install && npm run build
# build the xlsx_2_icalc binary (we don't need the release version here)
WORKDIR /app/xlsx
RUN cargo build
WORKDIR /app
# copy the artifacts to a dist/ directory
RUN mkdir dist
RUN mkdir dist/frontend
RUN cp -r webapp/app.ironcalc.com/frontend/dist/* dist/frontend/
RUN mkdir dist/server
RUN cp webapp/app.ironcalc.com/server/target/release/ironcalc_server dist/server/
RUN cp webapp/app.ironcalc.com/server/Rocket.toml dist/server/
RUN cp webapp/app.ironcalc.com/server/ironcalc.sqlite dist/server/
# Create ic files in docs
RUN mkdir -p dist/frontend/models
# Loop over all xlsx files in xlsx/tests/docs & templates and convert them to .ic
RUN bash -lc 'set -euo pipefail; \
mkdir -p dist/frontend/models; \
shopt -s nullglob; \
for xlsx_file in xlsx/tests/docs/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done; \
for xlsx_file in xlsx/tests/templates/*.xlsx; do \
base_name="${xlsx_file##*/}"; base_name="${base_name%.xlsx}"; \
./target/debug/xlsx_2_icalc "$xlsx_file" "dist/frontend/models/${base_name}.ic"; \
done'
# ---------- server runtime ----------
FROM debian:bookworm-slim AS server-runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy EVERYTHING you put in dist/server (binary + Rocket.toml + DB)
COPY --from=builder /app/dist/server/ ./
# Make sure Rocket binds to the container IP; explicitly point to the config file
ENV ROCKET_ADDRESS=0.0.0.0 \
ROCKET_PORT=8000 \
ROCKET_CONFIG=/app/Rocket.toml
EXPOSE 8000
# Run from /app so relative paths in Rocket.toml/DB work
CMD ["./ironcalc_server"]
# ---------- caddy runtime (serves frontend + reverse-proxy /api) ----------
FROM caddy:latest AS caddy-runtime
WORKDIR /srv
# Copy the frontend build output to /srv
COPY --from=builder /app/dist/frontend/ /srv/
# Copy the Caddyfile
COPY --from=builder /app/webapp/app.ironcalc.com/Caddyfile.compose /etc/caddy/Caddyfile

View File

@@ -31,17 +31,7 @@ This repository contains the main engine and the xlsx reader and writer.
Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go. Programmed in Rust, you will be able to use it from a variety of programming languages like Python, JavaScript (wasm), nodejs and possibly R, Julia or Go.
We will build different _skins_: in the terminal, as a desktop application or use it in your own web application. We will build different _skins_: in the terminal, as a desktop application or use it in you own web application.
# Docker
If you have docker installed just run:
```bash
docker compose up --build
```
head over to <http://localhost:2080> to test the application.
# Building # Building
@@ -94,7 +84,7 @@ And then use this code in `main.rs`:
```rust ```rust
use ironcalc::{ use ironcalc::{
base::{expressions::utils::number_to_column, Model}, base::{expressions::utils::number_to_column, model::Model},
export::save_to_xlsx, export::save_to_xlsx,
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 B

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.6.0" version = "0.5.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021" edition = "2021"
homepage = "https://www.ironcalc.com" homepage = "https://www.ironcalc.com"
@@ -19,7 +19,6 @@ regex = { version = "1.0", optional = true}
regex-lite = { version = "0.1.6", optional = true} regex-lite = { version = "0.1.6", optional = true}
bitcode = "0.6.3" bitcode = "0.6.3"
csv = "1.3.0" csv = "1.3.0"
statrs = { version = "0.18.0", default-features = false, features = [] }
[features] [features]
default = ["use_regex_full"] default = ["use_regex_full"]

View File

@@ -5,7 +5,6 @@ use crate::{
token::Error, token::Error,
types::CellReferenceIndex, types::CellReferenceIndex,
}, },
formatter::format::parse_formatted_number,
model::Model, model::Model,
}; };
@@ -15,23 +14,6 @@ pub(crate) enum NumberOrArray {
} }
impl Model { impl Model {
pub(crate) fn cast_number(&self, s: &str) -> Option<f64> {
match s.trim().parse::<f64>() {
Ok(f) => Some(f),
_ => {
let currency = &self.locale.currency.symbol;
let mut currencies = vec!["$", ""];
if !currencies.iter().any(|e| *e == currency) {
currencies.push(currency);
}
// Try to parse as a formatted number (e.g., dates, currencies, percentages)
if let Ok((v, _number_format)) = parse_formatted_number(s, &currencies) {
return Some(v);
}
None
}
}
}
pub(crate) fn get_number_or_array( pub(crate) fn get_number_or_array(
&mut self, &mut self,
node: &Node, node: &Node,
@@ -39,9 +21,9 @@ impl Model {
) -> Result<NumberOrArray, CalcResult> { ) -> Result<NumberOrArray, CalcResult> {
match self.evaluate_node_in_context(node, cell) { match self.evaluate_node_in_context(node, cell) {
CalcResult::Number(f) => Ok(NumberOrArray::Number(f)), CalcResult::Number(f) => Ok(NumberOrArray::Number(f)),
CalcResult::String(s) => match self.cast_number(&s) { CalcResult::String(s) => match s.parse::<f64>() {
Some(f) => Ok(NumberOrArray::Number(f)), Ok(f) => Ok(NumberOrArray::Number(f)),
None => Err(CalcResult::new_error( _ => Err(CalcResult::new_error(
Error::VALUE, Error::VALUE,
cell, cell,
"Expecting number".to_string(), "Expecting number".to_string(),
@@ -107,16 +89,16 @@ impl Model {
self.cast_to_number(result, cell) self.cast_to_number(result, cell)
} }
pub(crate) fn cast_to_number( fn cast_to_number(
&mut self, &mut self,
result: CalcResult, result: CalcResult,
cell: CellReferenceIndex, cell: CellReferenceIndex,
) -> Result<f64, CalcResult> { ) -> Result<f64, CalcResult> {
match result { match result {
CalcResult::Number(f) => Ok(f), CalcResult::Number(f) => Ok(f),
CalcResult::String(s) => match self.cast_number(&s) { CalcResult::String(s) => match s.parse::<f64>() {
Some(f) => Ok(f), Ok(f) => Ok(f),
None => Err(CalcResult::new_error( _ => Err(CalcResult::new_error(
Error::VALUE, Error::VALUE,
cell, cell,
"Expecting number".to_string(), "Expecting number".to_string(),

View File

@@ -341,8 +341,7 @@ fn static_analysis_offset(args: &[Node]) -> StaticResult {
} }
_ => return StaticResult::Unknown, _ => return StaticResult::Unknown,
}; };
// Both height and width are explicitly 1, so OFFSET will return a single cell StaticResult::Unknown
StaticResult::Scalar
} }
// fn static_analysis_choose(_args: &[Node]) -> StaticResult { // fn static_analysis_choose(_args: &[Node]) -> StaticResult {
@@ -576,37 +575,6 @@ fn args_signature_xnpv(arg_count: usize) -> Vec<Signature> {
} }
} }
// NETWORKDAYS(start_date, end_date, [holidays])
// Parameters: start_date (scalar), end_date (scalar), holidays (optional vector)
fn args_signature_networkdays(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Vector]
} else {
vec![Signature::Error; arg_count]
}
}
// NETWORKDAYS.INTL(start_date, end_date, [weekend], [holidays])
// Parameters: start_date (scalar), end_date (scalar), weekend (optional scalar), holidays (optional vector)
fn args_signature_networkdays_intl(arg_count: usize) -> Vec<Signature> {
if arg_count == 2 {
vec![Signature::Scalar, Signature::Scalar]
} else if arg_count == 3 {
vec![Signature::Scalar, Signature::Scalar, Signature::Scalar]
} else if arg_count == 4 {
vec![
Signature::Scalar,
Signature::Scalar,
Signature::Scalar,
Signature::Vector,
]
} else {
vec![Signature::Error; arg_count]
}
}
// FIXME: This is terrible duplications of efforts. We use the signature in at least three different places: // FIXME: This is terrible duplications of efforts. We use the signature in at least three different places:
// 1. When computing the function // 1. When computing the function
// 2. Checking the arguments to see if we need to insert the implicit intersection operator // 2. Checking the arguments to see if we need to insert the implicit intersection operator
@@ -722,28 +690,13 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Maxifs => vec![Signature::Vector; arg_count], Function::Maxifs => vec![Signature::Vector; arg_count],
Function::Minifs => vec![Signature::Vector; arg_count], Function::Minifs => vec![Signature::Vector; arg_count],
Function::Date => args_signature_scalars(arg_count, 3, 0), Function::Date => args_signature_scalars(arg_count, 3, 0),
Function::Datedif => args_signature_scalars(arg_count, 3, 0),
Function::Datevalue => args_signature_scalars(arg_count, 1, 0),
Function::Day => args_signature_scalars(arg_count, 1, 0), Function::Day => args_signature_scalars(arg_count, 1, 0),
Function::Edate => args_signature_scalars(arg_count, 2, 0), Function::Edate => args_signature_scalars(arg_count, 2, 0),
Function::Eomonth => args_signature_scalars(arg_count, 2, 0), Function::Eomonth => args_signature_scalars(arg_count, 2, 0),
Function::Month => args_signature_scalars(arg_count, 1, 0), Function::Month => args_signature_scalars(arg_count, 1, 0),
Function::Time => args_signature_scalars(arg_count, 3, 0),
Function::Timevalue => args_signature_scalars(arg_count, 1, 0),
Function::Hour => args_signature_scalars(arg_count, 1, 0),
Function::Minute => args_signature_scalars(arg_count, 1, 0),
Function::Second => args_signature_scalars(arg_count, 1, 0),
Function::Now => args_signature_no_args(arg_count), Function::Now => args_signature_no_args(arg_count),
Function::Today => args_signature_no_args(arg_count), Function::Today => args_signature_no_args(arg_count),
Function::Year => args_signature_scalars(arg_count, 1, 0), Function::Year => args_signature_scalars(arg_count, 1, 0),
Function::Days => args_signature_scalars(arg_count, 2, 0),
Function::Days360 => args_signature_scalars(arg_count, 2, 1),
Function::Weekday => args_signature_scalars(arg_count, 1, 1),
Function::Weeknum => args_signature_scalars(arg_count, 1, 1),
Function::Workday => args_signature_scalars(arg_count, 2, 1),
Function::WorkdayIntl => args_signature_scalars(arg_count, 2, 2),
Function::Yearfrac => args_signature_scalars(arg_count, 2, 1),
Function::Isoweeknum => args_signature_scalars(arg_count, 1, 0),
Function::Cumipmt => args_signature_scalars(arg_count, 6, 0), Function::Cumipmt => args_signature_scalars(arg_count, 6, 0),
Function::Cumprinc => args_signature_scalars(arg_count, 6, 0), Function::Cumprinc => args_signature_scalars(arg_count, 6, 0),
Function::Db => args_signature_scalars(arg_count, 4, 1), Function::Db => args_signature_scalars(arg_count, 4, 1),
@@ -832,63 +785,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Formulatext => args_signature_scalars(arg_count, 1, 0), Function::Formulatext => args_signature_scalars(arg_count, 1, 0),
Function::Unicode => args_signature_scalars(arg_count, 1, 0), Function::Unicode => args_signature_scalars(arg_count, 1, 0),
Function::Geomean => vec![Signature::Vector; arg_count], Function::Geomean => vec![Signature::Vector; arg_count],
Function::Networkdays => args_signature_networkdays(arg_count),
Function::NetworkdaysIntl => args_signature_networkdays_intl(arg_count),
Function::Acot => args_signature_scalars(arg_count, 1, 0),
Function::Acoth => args_signature_scalars(arg_count, 1, 0),
Function::Cot => args_signature_scalars(arg_count, 1, 0),
Function::Coth => args_signature_scalars(arg_count, 1, 0),
Function::Csc => args_signature_scalars(arg_count, 1, 0),
Function::Csch => args_signature_scalars(arg_count, 1, 0),
Function::Sec => args_signature_scalars(arg_count, 1, 0),
Function::Sech => args_signature_scalars(arg_count, 1, 0),
Function::Exp => args_signature_scalars(arg_count, 1, 0),
Function::Fact => args_signature_scalars(arg_count, 1, 0),
Function::Factdouble => args_signature_scalars(arg_count, 1, 0),
Function::Sign => args_signature_scalars(arg_count, 1, 0),
Function::Radians => args_signature_scalars(arg_count, 1, 0),
Function::Degrees => args_signature_scalars(arg_count, 1, 0),
Function::Int => args_signature_scalars(arg_count, 1, 0),
Function::Even => args_signature_scalars(arg_count, 1, 0),
Function::Odd => args_signature_scalars(arg_count, 1, 0),
Function::Ceiling => args_signature_scalars(arg_count, 2, 0),
Function::CeilingMath => args_signature_scalars(arg_count, 1, 2),
Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1),
Function::Floor => args_signature_scalars(arg_count, 2, 0),
Function::FloorMath => args_signature_scalars(arg_count, 1, 2),
Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1),
Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1),
Function::Mod => args_signature_scalars(arg_count, 2, 0),
Function::Quotient => args_signature_scalars(arg_count, 2, 0),
Function::Mround => args_signature_scalars(arg_count, 2, 0),
Function::Trunc => args_signature_scalars(arg_count, 1, 1),
Function::Gcd => vec![Signature::Vector; arg_count],
Function::Lcm => vec![Signature::Vector; arg_count],
Function::Base => args_signature_scalars(arg_count, 2, 1),
Function::Decimal => args_signature_scalars(arg_count, 2, 0),
Function::Roman => args_signature_scalars(arg_count, 1, 1),
Function::Arabic => args_signature_scalars(arg_count, 1, 0),
Function::Combin => args_signature_scalars(arg_count, 2, 0),
Function::Combina => args_signature_scalars(arg_count, 2, 0),
Function::Sumsq => vec![Signature::Vector; arg_count],
Function::N => args_signature_scalars(arg_count, 1, 0),
Function::Sheets => args_signature_scalars(arg_count, 0, 1),
Function::Cell => args_signature_scalars(arg_count, 1, 1),
Function::Info => args_signature_scalars(arg_count, 1, 1),
Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dmax => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dmin => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dsum => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dcounta => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dproduct => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dstdev => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dvar => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
} }
} }
@@ -1000,27 +896,12 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Maxifs => not_implemented(args), Function::Maxifs => not_implemented(args),
Function::Minifs => not_implemented(args), Function::Minifs => not_implemented(args),
Function::Date => not_implemented(args), Function::Date => not_implemented(args),
Function::Datedif => not_implemented(args),
Function::Datevalue => not_implemented(args),
Function::Day => not_implemented(args), Function::Day => not_implemented(args),
Function::Edate => not_implemented(args), Function::Edate => not_implemented(args),
Function::Month => not_implemented(args), Function::Month => not_implemented(args),
Function::Time => not_implemented(args),
Function::Timevalue => not_implemented(args),
Function::Hour => not_implemented(args),
Function::Minute => not_implemented(args),
Function::Second => not_implemented(args),
Function::Now => not_implemented(args), Function::Now => not_implemented(args),
Function::Today => not_implemented(args), Function::Today => not_implemented(args),
Function::Year => not_implemented(args), Function::Year => not_implemented(args),
Function::Days => not_implemented(args),
Function::Days360 => not_implemented(args),
Function::Weekday => not_implemented(args),
Function::Weeknum => not_implemented(args),
Function::Workday => not_implemented(args),
Function::WorkdayIntl => not_implemented(args),
Function::Yearfrac => not_implemented(args),
Function::Isoweeknum => not_implemented(args),
Function::Cumipmt => not_implemented(args), Function::Cumipmt => not_implemented(args),
Function::Cumprinc => not_implemented(args), Function::Cumprinc => not_implemented(args),
Function::Db => not_implemented(args), Function::Db => not_implemented(args),
@@ -1109,61 +990,5 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Eomonth => scalar_arguments(args), Function::Eomonth => scalar_arguments(args),
Function::Formulatext => not_implemented(args), Function::Formulatext => not_implemented(args),
Function::Geomean => not_implemented(args), Function::Geomean => not_implemented(args),
Function::Networkdays => not_implemented(args),
Function::NetworkdaysIntl => not_implemented(args),
Function::Acot => scalar_arguments(args),
Function::Acoth => scalar_arguments(args),
Function::Cot => scalar_arguments(args),
Function::Coth => scalar_arguments(args),
Function::Csc => scalar_arguments(args),
Function::Csch => scalar_arguments(args),
Function::Sec => scalar_arguments(args),
Function::Sech => scalar_arguments(args),
Function::Exp => scalar_arguments(args),
Function::Fact => scalar_arguments(args),
Function::Factdouble => scalar_arguments(args),
Function::Sign => scalar_arguments(args),
Function::Radians => scalar_arguments(args),
Function::Degrees => scalar_arguments(args),
Function::Int => scalar_arguments(args),
Function::Even => scalar_arguments(args),
Function::Odd => scalar_arguments(args),
Function::Ceiling => scalar_arguments(args),
Function::CeilingMath => scalar_arguments(args),
Function::CeilingPrecise => scalar_arguments(args),
Function::Floor => scalar_arguments(args),
Function::FloorMath => scalar_arguments(args),
Function::FloorPrecise => scalar_arguments(args),
Function::IsoCeiling => scalar_arguments(args),
Function::Mod => scalar_arguments(args),
Function::Quotient => scalar_arguments(args),
Function::Mround => scalar_arguments(args),
Function::Trunc => scalar_arguments(args),
Function::Gcd => not_implemented(args),
Function::Lcm => not_implemented(args),
Function::Base => scalar_arguments(args),
Function::Decimal => scalar_arguments(args),
Function::Roman => scalar_arguments(args),
Function::Arabic => scalar_arguments(args),
Function::Combin => scalar_arguments(args),
Function::Combina => scalar_arguments(args),
Function::Sumsq => StaticResult::Scalar,
Function::N => scalar_arguments(args),
Function::Sheets => scalar_arguments(args),
Function::Cell => scalar_arguments(args),
Function::Info => scalar_arguments(args),
Function::Dget => not_implemented(args),
Function::Dmax => not_implemented(args),
Function::Dmin => not_implemented(args),
Function::Dcount => not_implemented(args),
Function::Daverage => not_implemented(args),
Function::Dsum => not_implemented(args),
Function::Dcounta => not_implemented(args),
Function::Dproduct => not_implemented(args),
Function::Dstdev => not_implemented(args),
Function::Dvar => not_implemented(args),
Function::Dvarp => not_implemented(args),
Function::Dstdevp => not_implemented(args),
} }
} }

View File

@@ -520,7 +520,6 @@ fn stringify(
let x = match **left { let x = match **left {
BooleanKind(_) BooleanKind(_)
| NumberKind(_) | NumberKind(_)
| UnaryKind { .. }
| StringKind(_) | StringKind(_)
| ReferenceKind { .. } | ReferenceKind { .. }
| RangeKind { .. } | RangeKind { .. }
@@ -536,6 +535,7 @@ fn stringify(
| FunctionKind { .. } | FunctionKind { .. }
| InvalidFunctionKind { .. } | InvalidFunctionKind { .. }
| ArrayKind(_) | ArrayKind(_)
| UnaryKind { .. }
| ErrorKind(_) | ErrorKind(_)
| ParseErrorKind { .. } | ParseErrorKind { .. }
| OpSumKind { .. } | OpSumKind { .. }
@@ -630,6 +630,7 @@ fn stringify(
| OpRangeKind { .. } | OpRangeKind { .. }
| OpConcatenateKind { .. } | OpConcatenateKind { .. }
| OpProductKind { .. } | OpProductKind { .. }
| OpPowerKind { .. }
| FunctionKind { .. } | FunctionKind { .. }
| InvalidFunctionKind { .. } | InvalidFunctionKind { .. }
| ArrayKind(_) | ArrayKind(_)
@@ -642,7 +643,7 @@ fn stringify(
| ParseErrorKind { .. } | ParseErrorKind { .. }
| EmptyArgKind => false, | EmptyArgKind => false,
OpPowerKind { .. } | OpSumKind { .. } | UnaryKind { .. } => true, OpSumKind { .. } | UnaryKind { .. } => true,
}; };
if needs_parentheses { if needs_parentheses {
format!( format!(

View File

@@ -3,7 +3,6 @@ mod test_arrays;
mod test_general; mod test_general;
mod test_implicit_intersection; mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_issue_483;
mod test_move_formula; mod test_move_formula;
mod test_ranges; mod test_ranges;
mod test_stringify; mod test_stringify;

View File

@@ -1,27 +0,0 @@
#![allow(clippy::panic)]
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;
#[test]
fn issue_483_parser() {
let worksheets = vec!["Sheet1".to_string()];
let mut parser = Parser::new(worksheets, vec![], HashMap::new());
// Reference cell is Sheet1!A1
let cell_reference = CellReferenceRC {
sheet: "Sheet1".to_string(),
row: 2,
column: 2,
};
let t = parser.parse("-(A1^1.22)", &cell_reference);
assert!(matches!(t, Node::UnaryKind { .. }));
assert_eq!(to_string(&t, &cell_reference), "-(A1^1.22)");
let t = parser.parse("-A1^1.22", &cell_reference);
assert!(matches!(t, Node::OpPowerKind { .. }));
assert_eq!(to_string(&t, &cell_reference), "-A1^1.22");
}

View File

@@ -211,19 +211,15 @@ pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
pub fn is_valid_identifier(name: &str) -> bool { pub fn is_valid_identifier(name: &str) -> bool {
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1 // https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
// https://github.com/MartinTrummer/excel-names/ // https://github.com/MartinTrummer/excel-names/
// NOTE: We are being much more restrictive than Excel.
// In particular we do not support non ascii characters.
let upper = name.to_ascii_uppercase(); let upper = name.to_ascii_uppercase();
// length of chars let bytes = upper.as_bytes();
let len = upper.chars().count(); let len = bytes.len();
let mut chars = upper.chars();
if len > 255 || len == 0 { if len > 255 || len == 0 {
return false; return false;
} }
let first = match chars.next() { let first = bytes[0] as char;
Some(ch) => ch,
None => return false,
};
// The first character of a name must be a letter, an underscore character (_), or a backslash (\). // The first character of a name must be a letter, an underscore character (_), or a backslash (\).
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') { if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
return false; return false;
@@ -241,10 +237,20 @@ pub fn is_valid_identifier(name: &str) -> bool {
if parse_reference_r1c1(name).is_some() { if parse_reference_r1c1(name).is_some() {
return false; return false;
} }
for ch in chars { let mut i = 1;
if !(ch.is_alphanumeric() || ch == '_' || ch == '.') { while i < len {
return false; let ch = bytes[i] as char;
match ch {
'a'..='z' => {}
'A'..='Z' => {}
'0'..='9' => {}
'_' => {}
'.' => {}
_ => {
return false;
}
} }
i += 1;
} }
true true
@@ -253,23 +259,15 @@ pub fn is_valid_identifier(name: &str) -> bool {
fn name_needs_quoting(name: &str) -> bool { fn name_needs_quoting(name: &str) -> bool {
let chars = name.chars(); let chars = name.chars();
// it contains any of these characters: ()'$,;-+{} or space // it contains any of these characters: ()'$,;-+{} or space
for (i, char) in chars.enumerate() { for char in chars {
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) { if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
return true; return true;
} }
// if it starts with a number
if i == 0 && char.is_ascii_digit() {
return true;
}
}
if parse_reference_a1(name).is_some() {
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
return true;
}
if parse_reference_r1c1(name).is_some() {
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
return true;
} }
// TODO:
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
// integers
false false
} }
@@ -281,32 +279,3 @@ pub fn quote_name(name: &str) -> String {
}; };
name.to_string() name.to_string()
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_name() {
assert_eq!(quote_name("Sheet1"), "Sheet1");
assert_eq!(quote_name("Sheet 1"), "'Sheet 1'");
// escape and quote
assert_eq!(quote_name("Sheet1'"), "'Sheet1'''");
assert_eq!(quote_name("Data(2024)"), "'Data(2024)'");
assert_eq!(quote_name("Data$2024"), "'Data$2024'");
assert_eq!(quote_name("Data-2024"), "'Data-2024'");
assert_eq!(quote_name("Data+2024"), "'Data+2024'");
assert_eq!(quote_name("Data,2024"), "'Data,2024'");
assert_eq!(quote_name("Data;2024"), "'Data;2024'");
assert_eq!(quote_name("Data{2024}"), "'Data{2024}'");
assert_eq!(quote_name("2024"), "'2024'");
assert_eq!(quote_name("1Data"), "'1Data'");
assert_eq!(quote_name("A1"), "'A1'");
assert_eq!(quote_name("R1C1"), "'R1C1'");
assert_eq!(quote_name("MySheet"), "MySheet");
assert_eq!(quote_name("B1048576"), "'B1048576'");
assert_eq!(quote_name("B1048577"), "B1048577");
}
}

View File

@@ -196,7 +196,6 @@ fn test_names() {
assert!(is_valid_identifier("_.")); assert!(is_valid_identifier("_."));
assert!(is_valid_identifier("_1")); assert!(is_valid_identifier("_1"));
assert!(is_valid_identifier("\\.")); assert!(is_valid_identifier("\\."));
assert!(is_valid_identifier("truñe"));
// invalid // invalid
assert!(!is_valid_identifier("true")); assert!(!is_valid_identifier("true"));
@@ -210,6 +209,7 @@ fn test_names() {
assert!(!is_valid_identifier("1true")); assert!(!is_valid_identifier("1true"));
assert!(!is_valid_identifier("test€")); assert!(!is_valid_identifier("test€"));
assert!(!is_valid_identifier("truñe"));
assert!(!is_valid_identifier("tr&ue")); assert!(!is_valid_identifier("tr&ue"));
assert!(!is_valid_identifier("LOG10")); assert!(!is_valid_identifier("LOG10"));

View File

@@ -8,8 +8,6 @@ use crate::constants::EXCEL_DATE_BASE;
use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER; use crate::constants::MAXIMUM_DATE_SERIAL_NUMBER;
use crate::constants::MINIMUM_DATE_SERIAL_NUMBER; use crate::constants::MINIMUM_DATE_SERIAL_NUMBER;
pub const DATE_OUT_OF_RANGE_MESSAGE: &str = "Out of range parameters for date";
#[inline] #[inline]
fn convert_to_serial_number(date: NaiveDate) -> i32 { fn convert_to_serial_number(date: NaiveDate) -> i32 {
date.num_days_from_ce() - EXCEL_DATE_BASE date.num_days_from_ce() - EXCEL_DATE_BASE
@@ -39,7 +37,7 @@ pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> { pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
match NaiveDate::from_ymd_opt(year, month, day) { match NaiveDate::from_ymd_opt(year, month, day) {
Some(native_date) => Ok(convert_to_serial_number(native_date)), Some(native_date) => Ok(convert_to_serial_number(native_date)),
None => Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()), None => Err("Out of range parameters for date".to_string()),
} }
} }
@@ -57,7 +55,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
return Ok(MINIMUM_DATE_SERIAL_NUMBER); return Ok(MINIMUM_DATE_SERIAL_NUMBER);
} }
let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else { let Some(mut date) = NaiveDate::from_ymd_opt(year, 1, 1) else {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
}; };
// One thing to note for example is that even if you started with a year out of range // One thing to note for example is that even if you started with a year out of range
@@ -70,7 +68,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
// As a result, we have to run range checks as we parse the date from the biggest unit to the // As a result, we have to run range checks as we parse the date from the biggest unit to the
// smallest unit. // smallest unit.
if !is_date_within_range(date) { if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
} }
date = { date = {
@@ -82,7 +80,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
date = date + Months::new(abs_month); date = date + Months::new(abs_month);
} }
if !is_date_within_range(date) { if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
} }
date date
}; };
@@ -96,7 +94,7 @@ pub fn permissive_date_to_serial_number(day: i32, month: i32, year: i32) -> Resu
date = date + Days::new(abs_day); date = date + Days::new(abs_day);
} }
if !is_date_within_range(date) { if !is_date_within_range(date) {
return Err(DATE_OUT_OF_RANGE_MESSAGE.to_string()); return Err("Out of range parameters for date".to_string());
} }
date date
}; };

View File

@@ -15,7 +15,7 @@ pub struct Formatted {
/// Returns the vector of chars of the fractional part of a *positive* number: /// Returns the vector of chars of the fractional part of a *positive* number:
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6'] /// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> { fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
let b = format!("{:.1$}", value.fract(), precision as usize) let b = format!("{:.1$}", value.fract(), precision as usize)
.chars() .chars()
.collect::<Vec<char>>(); .collect::<Vec<char>>();
@@ -30,12 +30,6 @@ fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> {
if last_non_zero < 2 { if last_non_zero < 2 {
return vec![]; return vec![];
} }
let max_len = if int_len > 15 {
2_usize
} else {
15_usize - int_len + 1
};
let last_non_zero = usize::min(last_non_zero, max_len + 1);
b[2..last_non_zero].to_vec() b[2..last_non_zero].to_vec()
} }
@@ -160,11 +154,16 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
ParsePart::Date(p) => { ParsePart::Date(p) => {
let tokens = &p.tokens; let tokens = &p.tokens;
let mut text = "".to_string(); let mut text = "".to_string();
let time_fract = value.fract(); let date = match from_excel_date(value as i64) {
let hours = (time_fract * 24.0).floor(); Ok(d) => d,
let minutes = ((time_fract * 24.0 - hours) * 60.0).floor(); Err(e) => {
let seconds = ((((time_fract * 24.0 - hours) * 60.0) - minutes) * 60.0).round(); return Formatted {
let date = from_excel_date(value as i64).ok(); text: "#VALUE!".to_owned(),
color: None,
error: Some(e),
}
}
};
for token in tokens { for token in tokens {
match token { match token {
TextToken::Literal(c) => { TextToken::Literal(c) => {
@@ -188,44 +187,15 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
} }
TextToken::Digit(_) => {} TextToken::Digit(_) => {}
TextToken::Period => {} TextToken::Period => {}
TextToken::Day => match date { TextToken::Day => {
Some(date) => { let day = date.day() as usize;
let day = date.day() as usize; text = format!("{text}{day}");
text = format!("{text}{day}"); }
}
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
},
TextToken::DayPadded => { TextToken::DayPadded => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let day = date.day() as usize; let day = date.day() as usize;
text = format!("{text}{day:02}"); text = format!("{text}{day:02}");
} }
TextToken::DayNameShort => { TextToken::DayNameShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let mut day = date.weekday().number_from_monday() as usize; let mut day = date.weekday().number_from_monday() as usize;
if day == 7 { if day == 7 {
day = 0; day = 0;
@@ -233,16 +203,6 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names_short[day]); text = format!("{}{}", text, &locale.dates.day_names_short[day]);
} }
TextToken::DayName => { TextToken::DayName => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let mut day = date.weekday().number_from_monday() as usize; let mut day = date.weekday().number_from_monday() as usize;
if day == 7 { if day == 7 {
day = 0; day = 0;
@@ -250,144 +210,32 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text = format!("{}{}", text, &locale.dates.day_names[day]); text = format!("{}{}", text, &locale.dates.day_names[day]);
} }
TextToken::Month => { TextToken::Month => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{text}{month}"); text = format!("{text}{month}");
} }
TextToken::MonthPadded => { TextToken::MonthPadded => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{text}{month:02}"); text = format!("{text}{month:02}");
} }
TextToken::MonthNameShort => { TextToken::MonthNameShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months_short[month - 1]); text = format!("{}{}", text, &locale.dates.months_short[month - 1]);
} }
TextToken::MonthName => { TextToken::MonthName => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize; let month = date.month() as usize;
text = format!("{}{}", text, &locale.dates.months[month - 1]); text = format!("{}{}", text, &locale.dates.months[month - 1]);
} }
TextToken::MonthLetter => { TextToken::MonthLetter => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
let month = date.month() as usize; let month = date.month() as usize;
let months_letter = &locale.dates.months_letter[month - 1]; let months_letter = &locale.dates.months_letter[month - 1];
text = format!("{text}{months_letter}"); text = format!("{text}{months_letter}");
} }
TextToken::YearShort => { TextToken::YearShort => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
text = format!("{}{}", text, date.format("%y")); text = format!("{}{}", text, date.format("%y"));
} }
TextToken::Year => { TextToken::Year => {
let date = match date {
Some(d) => d,
None => {
return Formatted {
text: "#VALUE!".to_owned(),
color: None,
error: Some(format!("Invalid date value: '{value}'")),
}
}
};
text = format!("{}{}", text, date.year()); text = format!("{}{}", text, date.year());
} }
TextToken::Hour => {
let mut hour = hours as i32;
if p.use_ampm {
if hour == 0 {
hour = 12;
} else if hour > 12 {
hour -= 12;
}
}
text = format!("{text}{hour}");
}
TextToken::HourPadded => {
let mut hour = hours as i32;
if p.use_ampm {
if hour == 0 {
hour = 12;
} else if hour > 12 {
hour -= 12;
}
}
text = format!("{text}{hour:02}");
}
TextToken::Second => {
let second = seconds as i32;
text = format!("{text}{second}");
}
TextToken::SecondPadded => {
let second = seconds as i32;
text = format!("{text}{second:02}");
}
TextToken::AMPM => {
let ampm = if hours < 12.0 { "AM" } else { "PM" };
text = format!("{text}{ampm}");
}
TextToken::Minute => {
let minute = minutes as i32;
text = format!("{text}{minute}");
}
TextToken::MinutePadded => {
let minute = minutes as i32;
text = format!("{text}{minute:02}");
}
} }
} }
Formatted { Formatted {
@@ -429,7 +277,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if value_abs as i64 == 0 { if value_abs as i64 == 0 {
int_part = vec![]; int_part = vec![];
} }
let fract_part = get_fract_part(value_abs, p.precision, int_part.len()); let fract_part = get_fract_part(value_abs, p.precision);
// ln is the number of digits of the integer part of the value // ln is the number of digits of the integer part of the value
let ln = int_part.len() as i32; let ln = int_part.len() as i32;
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point // digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
@@ -574,13 +422,6 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
TextToken::MonthLetter => {} TextToken::MonthLetter => {}
TextToken::YearShort => {} TextToken::YearShort => {}
TextToken::Year => {} TextToken::Year => {}
TextToken::Hour => {}
TextToken::HourPadded => {}
TextToken::Minute => {}
TextToken::MinutePadded => {}
TextToken::Second => {}
TextToken::SecondPadded => {}
TextToken::AMPM => {}
} }
} }
Formatted { Formatted {
@@ -750,10 +591,10 @@ fn parse_date(value: &str) -> Result<(i32, String), String> {
/// "30.34%" => (0.3034, "0.00%") /// "30.34%" => (0.3034, "0.00%")
/// 100€ => (100, "100€") /// 100€ => (100, "100€")
pub(crate) fn parse_formatted_number( pub(crate) fn parse_formatted_number(
original: &str, value: &str,
currencies: &[&str], currencies: &[&str],
) -> Result<(f64, Option<String>), String> { ) -> Result<(f64, Option<String>), String> {
let value = original.trim(); let value = value.trim();
let scientific_format = "0.00E+00"; let scientific_format = "0.00E+00";
// Check if it is a percentage // Check if it is a percentage
@@ -805,8 +646,7 @@ pub(crate) fn parse_formatted_number(
} }
} }
// check if it is a date. NOTE: we don't trim the original here if let Ok((serial_number, format)) = parse_date(value) {
if let Ok((serial_number, format)) = parse_date(original) {
return Ok((serial_number as f64, Some(format))); return Ok((serial_number as f64, Some(format)));
} }

View File

@@ -26,23 +26,19 @@ pub enum Token {
Scientific, // E+ Scientific, // E+
ScientificMinus, // E- ScientificMinus, // E-
General, // General General, // General
// Dates and time // Dates
Day, // d Day, // d
DayPadded, // dd DayPadded, // dd
DayNameShort, // ddd DayNameShort, // ddd
DayName, // dddd+ DayName, // dddd+
Month, // m (or minute) Month, // m
MonthPadded, // mm (or minute padded) MonthPadded, // mm
MonthNameShort, // mmm MonthNameShort, // mmm
MonthName, // mmmm or mmmmmm+ MonthName, // mmmm or mmmmmm+
MonthLetter, // mmmmm MonthLetter, // mmmmm
YearShort, // y or yy YearShort, // y or yy
Year, // yyy+ Year, // yyy+
Hour, // h // TODO: Hours Minutes and Seconds
HourPadded, // hh
Second, // s
SecondPadded, // ss
AMPM, // AM/PM (or A/P)
ILLEGAL, ILLEGAL,
EOF, EOF,
} }
@@ -365,8 +361,8 @@ impl Lexer {
self.read_next_char(); self.read_next_char();
} }
match m { match m {
1 => Token::Month, // (or minute) 1 => Token::Month,
2 => Token::MonthPadded, // (or minute padded) 2 => Token::MonthPadded,
3 => Token::MonthNameShort, 3 => Token::MonthNameShort,
4 => Token::MonthName, 4 => Token::MonthName,
5 => Token::MonthLetter, 5 => Token::MonthLetter,
@@ -385,63 +381,6 @@ impl Lexer {
Token::Year Token::Year
} }
} }
'h' => {
let mut h = 1;
while let Some('h') = self.peek_char() {
h += 1;
self.read_next_char();
}
if h == 1 {
Token::Hour
} else if h == 2 {
Token::HourPadded
} else {
self.set_error("Unexpected character after 'h'");
Token::ILLEGAL
}
}
's' => {
let mut s = 1;
while let Some('s') = self.peek_char() {
s += 1;
self.read_next_char();
}
if s == 1 {
Token::Second
} else if s == 2 {
Token::SecondPadded
} else {
self.set_error("Unexpected character after 's'");
Token::ILLEGAL
}
}
'A' | 'a' => {
if let Some('M') | Some('m') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'A'");
return Token::ILLEGAL;
}
if let Some('/') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AM'");
return Token::ILLEGAL;
}
if let Some('P') | Some('p') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AM'");
return Token::ILLEGAL;
}
if let Some('M') | Some('m') = self.peek_char() {
self.read_next_char();
} else {
self.set_error("Unexpected character after 'AMP'");
return Token::ILLEGAL;
}
Token::AMPM
}
'g' | 'G' => { 'g' | 'G' => {
for c in "eneral".chars() { for c in "eneral".chars() {
let cc = self.read_next_char(); let cc = self.read_next_char();

View File

@@ -27,13 +27,6 @@ pub enum TextToken {
MonthLetter, MonthLetter,
YearShort, YearShort,
Year, Year,
Hour,
HourPadded,
Minute,
MinutePadded,
Second,
SecondPadded,
AMPM,
} }
pub struct NumberPart { pub struct NumberPart {
pub color: Option<i32>, pub color: Option<i32>,
@@ -52,7 +45,6 @@ pub struct NumberPart {
pub struct DatePart { pub struct DatePart {
pub color: Option<i32>, pub color: Option<i32>,
pub use_ampm: bool,
pub tokens: Vec<TextToken>, pub tokens: Vec<TextToken>,
} }
@@ -109,7 +101,6 @@ impl Parser {
let mut digit_count = 0; let mut digit_count = 0;
let mut precision = 0; let mut precision = 0;
let mut is_date = false; let mut is_date = false;
let mut use_ampm = false;
let mut is_number = false; let mut is_number = false;
let mut found_decimal_dot = false; let mut found_decimal_dot = false;
let mut use_thousands = false; let mut use_thousands = false;
@@ -125,7 +116,6 @@ impl Parser {
let mut number = 'i'; let mut number = 'i';
let mut index = 0; let mut index = 0;
let mut currency = None; let mut currency = None;
let mut is_time = false;
while token != Token::EOF && token != Token::Separator { while token != Token::EOF && token != Token::Separator {
let next_token = self.lexer.next_token(); let next_token = self.lexer.next_token();
@@ -210,9 +200,6 @@ impl Parser {
index += 1; index += 1;
} }
Token::Literal(value) => { Token::Literal(value) => {
if value == ':' {
is_time = true;
}
tokens.push(TextToken::Literal(value)); tokens.push(TextToken::Literal(value));
} }
Token::Text(value) => { Token::Text(value) => {
@@ -249,22 +236,12 @@ impl Parser {
tokens.push(TextToken::MonthName); tokens.push(TextToken::MonthName);
} }
Token::Month => { Token::Month => {
if is_time { is_date = true;
// minute tokens.push(TextToken::Month);
tokens.push(TextToken::Minute);
} else {
is_date = true;
tokens.push(TextToken::Month);
}
} }
Token::MonthPadded => { Token::MonthPadded => {
if is_time { is_date = true;
// minute padded tokens.push(TextToken::MonthPadded);
tokens.push(TextToken::MinutePadded);
} else {
is_date = true;
tokens.push(TextToken::MonthPadded);
}
} }
Token::MonthLetter => { Token::MonthLetter => {
is_date = true; is_date = true;
@@ -278,32 +255,6 @@ impl Parser {
is_date = true; is_date = true;
tokens.push(TextToken::Year); tokens.push(TextToken::Year);
} }
Token::Hour => {
is_date = true;
is_time = true;
tokens.push(TextToken::Hour);
}
Token::HourPadded => {
is_date = true;
is_time = true;
tokens.push(TextToken::HourPadded);
}
Token::Second => {
is_date = true;
is_time = true;
tokens.push(TextToken::Second);
}
Token::SecondPadded => {
is_date = true;
is_time = true;
tokens.push(TextToken::SecondPadded);
}
Token::AMPM => {
is_date = true;
use_ampm = true;
tokens.push(TextToken::AMPM);
}
Token::Scientific => { Token::Scientific => {
if !is_scientific { if !is_scientific {
index = 0; index = 0;
@@ -331,11 +282,7 @@ impl Parser {
if is_number { if is_number {
return ParsePart::Error(ErrorPart {}); return ParsePart::Error(ErrorPart {});
} }
ParsePart::Date(DatePart { ParsePart::Date(DatePart { color, tokens })
color,
use_ampm,
tokens,
})
} else { } else {
ParsePart::Number(NumberPart { ParsePart::Number(NumberPart {
color, color,

View File

@@ -1,3 +1,2 @@
mod test_general; mod test_general;
mod test_parse_formatted_number; mod test_parse_formatted_number;
mod test_time;

View File

@@ -1,32 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::{
formatter::format::format_number,
locale::{get_locale, Locale},
};
fn get_default_locale() -> &'static Locale {
get_locale("en").unwrap()
}
#[test]
fn simple_test() {
let locale = get_default_locale();
let format = "h:mm AM/PM";
let value = 16.001_423_611_111_11; // =1/86400 => 12:02 AM
let formatted = format_number(value, format, locale);
assert_eq!(formatted.text, "12:02 AM");
}
#[test]
fn padded_vs_unpadded() {
let locale = get_default_locale();
let padded_format = "hh:mm:ss AM/PM";
let unpadded_format = "h:m:s AM/PM";
let value = 0.25351851851851853; // => 6:05:04 AM (21904/(24*60*60)) where 21904 = 6 * 3600 + 5*60 + 4
let formatted = format_number(value, padded_format, locale);
assert_eq!(formatted.text, "06:05:04 AM");
let formatted = format_number(value, unpadded_format, locale);
assert_eq!(formatted.text, "6:5:4 AM");
}

View File

@@ -1,946 +0,0 @@
use chrono::Datelike;
use crate::{
calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
formatter::dates::date_to_serial_number,
Model,
};
use super::util::{compare_values, from_wildcard_to_regex, result_matches_regex};
impl Model {
// =DAVERAGE(database, field, criteria)
pub(crate) fn fn_daverage(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut sum = 0.0f64;
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
sum += n;
count += 1;
}
}
}
row += 1;
}
if count == 0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "No numeric values matched criteria".to_string(),
};
}
CalcResult::Number(sum / count as f64)
}
// =DCOUNT(database, field, criteria)
// Counts numeric entries in the field for rows that match criteria
pub(crate) fn fn_dcount(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if matches!(v, CalcResult::Number(_)) {
count += 1;
}
}
row += 1;
}
CalcResult::Number(count as f64)
}
// =DGET(database, field, criteria)
// Returns the (single) field value for the unique matching row
pub(crate) fn fn_dget(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut result: Option<CalcResult> = None;
let mut matches = 0usize;
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
matches += 1;
if matches > 1 {
return CalcResult::Error {
error: Error::NUM,
origin: cell,
message: "More than one matching record".to_string(),
};
}
result = Some(self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
}));
}
row += 1;
}
match (matches, result) {
(0, _) | (_, None) => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No matching record".to_string(),
},
(_, Some(v)) => v,
}
}
// =DMAX(database, field, criteria)
pub(crate) fn fn_dmax(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.db_extreme(args, cell, true)
}
// =DMIN(database, field, criteria)
pub(crate) fn fn_dmin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.db_extreme(args, cell, false)
}
// =DSUM(database, field, criteria)
pub(crate) fn fn_dsum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut sum = 0.0;
// skip header
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
sum += n;
}
}
}
row += 1;
}
CalcResult::Number(sum)
}
// =DCOUNTA(database, field, criteria)
// Counts non-empty entries (any type) in the field for rows that match criteria
pub(crate) fn fn_dcounta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut count = 0;
for row in (db_left.row + 1)..=db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if !matches!(v, CalcResult::EmptyCell | CalcResult::EmptyArg) {
count += 1;
}
}
}
CalcResult::Number(count as f64)
}
// =DPRODUCT(database, field, criteria)
pub(crate) fn fn_dproduct(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut product = 1.0f64;
let mut has_numeric = false;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
product *= n;
has_numeric = true;
}
}
}
row += 1;
}
// Excel returns 0 when no rows / no numeric values match for DPRODUCT
if has_numeric {
CalcResult::Number(product)
} else {
CalcResult::Number(0.0)
}
}
// Small internal helper for DSTDEV / DVAR
// Collects sum, sum of squares, and count of numeric values in the field
// for rows that match the criteria.
fn db_numeric_stats(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> Result<(f64, f64, usize), CalcResult> {
if args.len() != 3 {
return Err(CalcResult::new_args_number_error(cell));
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return Err(e),
};
let field_col = self.resolve_db_field_column(db_left, db_right, &args[1], cell)?;
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return Err(e),
};
if db_right.row <= db_left.row {
// no data rows
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
});
}
let mut sum = 0.0f64;
let mut sumsq = 0.0f64;
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
sum += n;
sumsq += n * n;
count += 1;
}
}
}
row += 1;
}
Ok((sum, sumsq, count))
}
// =DSTDEV(database, field, criteria)
// Sample standard deviation of matching numeric values
pub(crate) fn fn_dstdev(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
if count < 2 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Not enough numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var.sqrt())
}
// =DVAR(database, field, criteria)
// Sample variance of matching numeric values
pub(crate) fn fn_dvar(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if 0 or 1 numeric values match
if count < 2 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "Not enough numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / (n - 1.0);
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var)
}
// =DSTDEVP(database, field, criteria)
// Population standard deviation of matching numeric values
pub(crate) fn fn_dstdevp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if no numeric values match
if count == 0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "No numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / n;
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var.sqrt())
}
// =DVARP(database, field, criteria)
// Population variance of matching numeric values
pub(crate) fn fn_dvarp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) {
Ok(stats) => stats,
Err(e) => return e,
};
// Excel behaviour: #DIV/0! if no numeric values match
if count == 0 {
return CalcResult::Error {
error: Error::DIV,
origin: cell,
message: "No numeric values matched criteria".to_string(),
};
}
let n = count as f64;
let var = (sumsq - (sum * sum) / n) / n;
let var = if var < 0.0 { 0.0 } else { var };
CalcResult::Number(var)
}
/// Resolve the "field" (2nd arg) to an absolute column index (i32) within the sheet.
/// Field can be a number (1-based index) or a header name (case-insensitive).
/// Returns the absolute column index, not a 1-based offset within the database range.
fn resolve_db_field_column(
&mut self,
db_left: CellReferenceIndex,
db_right: CellReferenceIndex,
field_arg: &Node,
cell: CellReferenceIndex,
) -> Result<i32, CalcResult> {
let field_column_name = match self.evaluate_node_in_context(field_arg, cell) {
CalcResult::String(s) => s.to_lowercase(),
CalcResult::Number(index) => {
let index = index.floor() as i32;
if index < 1 || db_left.column + index - 1 > db_right.column {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Field index out of range".to_string(),
});
}
return Ok(db_left.column + index - 1);
}
CalcResult::Boolean(b) => {
return if b {
Ok(db_left.column)
} else {
// Index 0 is out of range
Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid field specifier".to_string(),
})
};
}
error @ CalcResult::Error { .. } => {
return Err(error);
}
CalcResult::Range { .. } => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
};
// We search in the database a column whose header matches field_column_name
for column in db_left.column..=db_right.column {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row: db_left.row,
column,
});
match &v {
CalcResult::String(s) => {
if s.to_lowercase() == field_column_name {
return Ok(column);
}
}
CalcResult::Number(n) => {
if field_column_name == n.to_string() {
return Ok(column);
}
}
CalcResult::Boolean(b) => {
if field_column_name == b.to_string() {
return Ok(column);
}
}
CalcResult::Error { .. }
| CalcResult::Range { .. }
| CalcResult::EmptyCell
| CalcResult::EmptyArg
| CalcResult::Array(_) => {}
}
}
Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Field header not found".to_string(),
})
}
/// Check whether a database row matches the criteria range.
/// Criteria logic: OR across criteria rows; AND across columns within a row.
fn db_row_matches_criteria(
&mut self,
db_left: CellReferenceIndex,
db_right: CellReferenceIndex,
row: i32,
criteria: (CellReferenceIndex, CellReferenceIndex),
) -> bool {
let (c_left, c_right) = criteria;
// Read criteria headers (first row of criteria range)
// Map header name (lowercased) -> db column (if exists)
let mut crit_cols: Vec<i32> = Vec::new();
let mut header_count = 0;
// We cover the criteria table:
// headerA | headerB | ...
// critA1 | critA2 | ...
// critB1 | critB2 | ...
// ...
for column in c_left.column..=c_right.column {
let cell = CellReferenceIndex {
sheet: c_left.sheet,
row: c_left.row,
column,
};
let criteria_header = self.evaluate_cell(cell);
if let Ok(s) = self.cast_to_string(criteria_header, cell) {
// Non-empty string header. If the header is non string we skip it
header_count += 1;
let wanted = s.to_lowercase();
// Find corresponding Database column
let mut found = false;
for db_column in db_left.column..=db_right.column {
let db_header = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row: db_left.row,
column: db_column,
});
if let Ok(hs) = self.cast_to_string(db_header, cell) {
if hs.to_lowercase() == wanted {
crit_cols.push(db_column);
found = true;
break;
}
}
}
if !found {
// that means the criteria column has no matching DB column
// If the criteria condition is empty then we remove this condition
// otherwise this condition can never be satisfied
// We evaluate all criteria rows to see if any is non-empty
let mut has_non_empty = false;
for r in (c_left.row + 1)..=c_right.row {
let ccell = self.evaluate_cell(CellReferenceIndex {
sheet: c_left.sheet,
row: r,
column,
});
if !matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
has_non_empty = true;
break;
}
}
if has_non_empty {
// This criteria column can never be satisfied
header_count -= 1;
}
}
};
}
if c_right.row <= c_left.row {
// If no criteria rows (only headers), everything matches
return true;
}
if header_count == 0 {
// If there are not "String" headers, nothing matches
// NB: There might be String headers that do not match any DB columns,
// in that case everything matches.
return false;
}
// Evaluate each criteria row (OR)
for r in (c_left.row + 1)..=c_right.row {
// AND across columns for this criteria row
let mut and_ok = true;
for (offset, db_col) in crit_cols.iter().enumerate() {
// Criteria cell
let ccell = self.evaluate_cell(CellReferenceIndex {
sheet: c_left.sheet,
row: r,
column: c_left.column + offset as i32,
});
// Empty criteria cell -> ignored
if matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
continue;
}
// Database value for this row/column
let db_val = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: *db_col,
});
if !self.criteria_cell_matches(&db_val, &ccell) {
and_ok = false;
break;
}
}
if and_ok {
// This criteria row satisfied (OR)
return true;
}
}
// none matched
false
}
/// Implements Excel-like criteria matching for a single value.
/// Supports prefixes: <>, >=, <=, >, <, = ; wildcards * and ? for string equals.
fn criteria_cell_matches(&self, db_val: &CalcResult, crit_cell: &CalcResult) -> bool {
// Convert the criteria cell to a string for operator parsing if possible,
// otherwise fall back to equality via compare_values.
let mut criteria = match crit_cell {
CalcResult::String(s) => s.trim().to_string(),
CalcResult::Number(n) => {
// treat as equality with number
return match db_val {
CalcResult::Number(v) => (*v - *n).abs() <= f64::EPSILON,
_ => false,
};
}
CalcResult::Boolean(b) => {
// check equality with boolean
return match db_val {
CalcResult::Boolean(v) => *v == *b,
_ => false,
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Error { .. } => return false,
CalcResult::Range { .. } | CalcResult::Array(_) => return false,
};
// Detect operator prefix
let mut op = "="; // default equality (with wildcard semantics for strings)
let prefixes = ["<>", ">=", "<=", ">", "<", "="];
for p in prefixes.iter() {
if criteria.starts_with(p) {
op = p;
criteria = criteria[p.len()..].trim().to_string();
break;
}
}
// Is it a number?
let rhs_num = criteria.parse::<f64>().ok();
// Is it a date?
// FIXME: We should parse dates according to locale settings
let rhs_date = criteria.parse::<chrono::NaiveDate>().ok();
match op {
">" | ">=" | "<" | "<=" => {
if let Some(d) = rhs_date {
// date comparison
let serial = match date_to_serial_number(d.day(), d.month(), d.year()) {
Ok(sn) => sn as f64,
Err(_) => return false,
};
if let CalcResult::Number(n) = db_val {
match op {
">" => *n > serial,
">=" => *n >= serial,
"<" => *n < serial,
"<=" => *n <= serial,
_ => false,
}
} else {
false
}
} else if let Some(t) = rhs_num {
// numeric comparison
if let CalcResult::Number(n) = db_val {
match op {
">" => *n > t,
">=" => *n >= t,
"<" => *n < t,
"<=" => *n <= t,
_ => false,
}
} else {
false
}
} else {
// string comparison (case-insensitive) using compare_values semantics
let rhs = CalcResult::String(criteria.to_lowercase());
let lhs = match db_val {
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
x => x.clone(),
};
let c = compare_values(&lhs, &rhs);
match op {
">" => c > 0,
">=" => c >= 0,
"<" => c < 0,
"<=" => c <= 0,
_ => false,
}
}
}
"<>" => {
// not equal (with wildcard semantics for strings)
// If rhs has wildcards and db_val is string, do regex; else use compare_values != 0
if let CalcResult::String(s) = db_val {
if criteria.contains('*') || criteria.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
return !result_matches_regex(
&CalcResult::String(s.to_lowercase()),
&re,
);
}
}
}
let rhs = if let Some(n) = rhs_num {
CalcResult::Number(n)
} else {
CalcResult::String(criteria.to_lowercase())
};
let lhs = match db_val {
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
x => x.clone(),
};
compare_values(&lhs, &rhs) != 0
}
_ => {
// equality. For strings, support wildcards (*, ?)
if let Some(n) = rhs_num {
// numeric equals
if let CalcResult::Number(m) = db_val {
(*m - n).abs() <= f64::EPSILON
} else {
compare_values(db_val, &CalcResult::Number(n)) == 0
}
} else {
// textual/boolean equals (case-insensitive), wildcard-enabled for strings
if let CalcResult::String(s) = db_val {
if criteria.contains('*') || criteria.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
return result_matches_regex(
&CalcResult::String(s.to_lowercase()),
&re,
);
}
}
// This is weird but we only need to check if "starts with" for equality
return s.to_lowercase().starts_with(&criteria.to_lowercase());
}
// Fallback: compare_values equality
compare_values(db_val, &CalcResult::String(criteria.to_lowercase())) == 0
}
}
}
}
/// Shared implementation for DMAX/DMIN
fn db_extreme(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
want_max: bool,
) -> CalcResult {
if args.len() != 3 {
return CalcResult::new_args_number_error(cell);
}
let (db_left, db_right) = match self.get_reference(&args[0], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) {
Ok(c) => c,
Err(e) => return e,
};
let criteria = match self.get_reference(&args[2], cell) {
Ok(r) => (r.left, r.right),
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut best: Option<f64> = None;
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: field_col,
});
if let CalcResult::Number(value) = v {
if value.is_finite() {
best = Some(match best {
None => value,
Some(cur) => {
if want_max {
value.max(cur)
} else {
value.min(cur)
}
}
});
}
}
}
row += 1;
}
match best {
Some(v) => CalcResult::Number(v),
None => CalcResult::Number(0.0),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
use statrs::function::erf::{erf, erfc};
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex}, expressions::{parser::Node, token::Error, types::CellReferenceIndex},
model::Model, model::Model,
}; };
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y}; use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y, erf};
// https://root.cern/doc/v610/TMath_8cxx_source.html // https://root.cern/doc/v610/TMath_8cxx_source.html
// Notice that the parameters for Bessel functions in Excel and here have inverted order // Notice that the parameters for Bessel functions in Excel and here have inverted order
@@ -162,7 +160,7 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
CalcResult::Number(erfc(x)) CalcResult::Number(1.0 - erf(x))
} }
pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
@@ -173,6 +171,6 @@ impl Model {
Ok(f) => f, Ok(f) => f,
Err(s) => return s, Err(s) => return s,
}; };
CalcResult::Number(erfc(x)) CalcResult::Number(1.0 - erf(x))
} }
} }

View File

@@ -0,0 +1,53 @@
pub(crate) fn erf(x: f64) -> f64 {
let cof = vec![
-1.3026537197817094,
6.419_697_923_564_902e-1,
1.9476473204185836e-2,
-9.561_514_786_808_63e-3,
-9.46595344482036e-4,
3.66839497852761e-4,
4.2523324806907e-5,
-2.0278578112534e-5,
-1.624290004647e-6,
1.303655835580e-6,
1.5626441722e-8,
-8.5238095915e-8,
6.529054439e-9,
5.059343495e-9,
-9.91364156e-10,
-2.27365122e-10,
9.6467911e-11,
2.394038e-12,
-6.886027e-12,
8.94487e-13,
3.13092e-13,
-1.12708e-13,
3.81e-16,
7.106e-15,
-1.523e-15,
-9.4e-17,
1.21e-16,
-2.8e-17,
];
let mut d = 0.0;
let mut dd = 0.0;
let x_abs = x.abs();
let t = 2.0 / (2.0 + x_abs);
let ty = 4.0 * t - 2.0;
for j in (1..=cof.len() - 1).rev() {
let tmp = d;
d = ty * d - dd + cof[j];
dd = tmp;
}
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
if x < 0.0 {
res - 1.0
} else {
1.0 - res
}
}

View File

@@ -4,6 +4,7 @@ mod bessel_j1_y1;
mod bessel_jn_yn; mod bessel_jn_yn;
mod bessel_k; mod bessel_k;
mod bessel_util; mod bessel_util;
mod erf;
#[cfg(test)] #[cfg(test)]
mod test_bessel; mod test_bessel;
@@ -12,3 +13,4 @@ pub(crate) use bessel_i::bessel_i;
pub(crate) use bessel_jn_yn::jn as bessel_j; pub(crate) use bessel_jn_yn::jn as bessel_j;
pub(crate) use bessel_jn_yn::yn as bessel_y; pub(crate) use bessel_jn_yn::yn as bessel_y;
pub(crate) use bessel_k::bessel_k; pub(crate) use bessel_k::bessel_k;
pub(crate) use erf::erf;

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
calc_result::CalcResult, calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex, utils::number_to_column}, expressions::{parser::Node, token::Error, types::CellReferenceIndex},
model::{Model, ParsedDefinedName}, model::{Model, ParsedDefinedName},
}; };
@@ -320,150 +320,4 @@ impl Model {
message: "Invalid name".to_string(), message: "Invalid name".to_string(),
} }
} }
pub(crate) fn fn_n(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count != 1 {
return CalcResult::new_args_number_error(cell);
}
let value = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(n) => n,
CalcResult::String(_) => 0.0,
CalcResult::Boolean(f) => {
if f {
1.0
} else {
0.0
}
}
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
CalcResult::Number(value)
}
pub(crate) fn fn_sheets(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count > 1 {
return CalcResult::new_args_number_error(cell);
}
if arg_count == 1 {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Sheets function with an argument is not implemented".to_string(),
};
}
let sheet_count = self.workbook.worksheets.len() as f64;
CalcResult::Number(sheet_count)
}
pub(crate) fn fn_cell(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
let arg_count = args.len();
if arg_count == 0 || arg_count > 2 {
return CalcResult::new_args_number_error(cell);
}
let reference = if arg_count == 2 {
match self.evaluate_node_with_reference(&args[1], cell) {
CalcResult::Range { left, right: _ } => {
// we just take the left cell of the range
left
}
_ => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Argument must be a reference".to_string(),
}
}
}
} else {
CellReferenceIndex {
sheet: cell.sheet,
row: cell.row,
column: cell.column,
}
};
let info_type = match self.get_string(&args[0], cell) {
Ok(s) => s.to_uppercase(),
Err(e) => return e,
};
match info_type.as_str() {
"ADDRESS" => {
if reference.sheet != cell.sheet {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "References to other sheets not implemented".to_string(),
};
}
let column = match number_to_column(reference.column) {
Some(c) => c,
None => {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid column".to_string(),
}
}
};
let address = format!("${}${}", column, reference.row);
CalcResult::String(address)
}
"COL" => CalcResult::Number(reference.column as f64),
"COLOR" | "FILENAME" | "FORMAT" | "PARENTHESES" | "PREFIX" | "PROTECT" | "WIDTH" => {
CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "info_type not implemented".to_string(),
}
}
"CONTENTS" => self.evaluate_cell(reference),
"ROW" => CalcResult::Number(reference.row as f64),
"TYPE" => {
let cell_type = match self.evaluate_cell(reference) {
CalcResult::EmptyCell => "b",
CalcResult::String(_) => "l",
CalcResult::Number(_) => "v",
CalcResult::Boolean(_) => "v",
CalcResult::Error { .. } => "v",
CalcResult::Range { .. } => "v",
CalcResult::EmptyArg => "v",
CalcResult::Array(_) => "v",
};
CalcResult::String(cell_type.to_string())
}
_ => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid info_type".to_string(),
},
}
}
pub(crate) fn fn_info(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() || args.len() > 2 {
return CalcResult::new_args_number_error(cell);
}
CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Info function not implemented".to_string(),
}
}
} }

View File

@@ -68,14 +68,14 @@ macro_rules! single_number_fn {
}, },
// If String, parse to f64 then apply or #VALUE! error // If String, parse to f64 then apply or #VALUE! error
ArrayNode::String(s) => { ArrayNode::String(s) => {
let node = match self.cast_number(&s) { let node = match s.parse::<f64>() {
Some(f) => match $op(f) { Ok(f) => match $op(f) {
Ok(x) => ArrayNode::Number(x), Ok(x) => ArrayNode::Number(x),
Err(Error::DIV) => ArrayNode::Error(Error::DIV), Err(Error::DIV) => ArrayNode::Error(Error::DIV),
Err(Error::VALUE) => ArrayNode::Error(Error::VALUE), Err(Error::VALUE) => ArrayNode::Error(Error::VALUE),
Err(e) => ArrayNode::Error(e), Err(e) => ArrayNode::Error(e),
}, },
None => ArrayNode::Error(Error::VALUE), Err(_) => ArrayNode::Error(Error::VALUE),
}; };
data_row.push(node); data_row.push(node);
} }

View File

@@ -1,200 +0,0 @@
/// Parse Roman (classic or Excel variants) → number
pub fn from_roman(s: &str) -> Result<u32, String> {
if s.is_empty() {
return Err("empty numeral".into());
}
fn val(c: char) -> Option<u32> {
Some(match c {
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
_ => return None,
})
}
// Accept the union of subtractive pairs used by the tables above (Excel-compatible).
fn allowed_subtractive(a: char, b: char) -> bool {
matches!(
(a, b),
// classic:
('I','V')|('I','X')|('X','L')|('X','C')|('C','D')|('C','M')
// Excel forms:
|('V','L')|('L','D')|('L','M') // VL, LD, LM
|('X','D')|('X','M') // XD, XM
|('V','M') // VM
|('I','L')|('I','C')|('I','D')|('I','M') // IL, IC, ID, IM
|('V','D')|('V','C') // VD, VC
)
}
let chars: Vec<char> = s.chars().map(|c| c.to_ascii_uppercase()).collect();
let mut total = 0u32;
let mut i = 0usize;
// Repetition rules similar to classic Romans:
// V, L, D cannot repeat; I, X, C, M max 3 in a row.
let mut last_char: Option<char> = None;
let mut run_len = 0usize;
while i < chars.len() {
let c = chars[i];
let v = val(c).ok_or_else(|| format!("invalid character '{c}'"))?;
if Some(c) == last_char {
run_len += 1;
match c {
'V' | 'L' | 'D' => return Err(format!("invalid repetition of '{c}'")),
_ if run_len >= 3 => return Err(format!("invalid repetition of '{c}'")),
_ => {}
}
} else {
last_char = Some(c);
run_len = 0;
}
if i + 1 < chars.len() {
let c2 = chars[i + 1];
let v2 = val(c2).ok_or_else(|| format!("invalid character '{c2}'"))?;
if v < v2 {
if !allowed_subtractive(c, c2) {
return Err(format!("invalid subtractive pair '{c}{c2}'"));
}
// Disallow stacked subtractives like IIV, XXL:
if run_len > 0 {
return Err(format!("malformed numeral near position {i}"));
}
total += v2 - v;
i += 2;
last_char = None;
run_len = 0;
continue;
}
}
total += v;
i += 1;
}
Ok(total)
}
/// Classic Roman (strict) encoder used as a base for all forms.
fn to_roman(mut n: u32) -> Result<String, String> {
if !(1..=3999).contains(&n) {
return Err("value out of range (must be 1..=3999)".into());
}
const MAP: &[(u32, &str)] = &[
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut out = String::with_capacity(15);
for &(val, sym) in MAP {
while n >= val {
out.push_str(sym);
n -= val;
}
if n == 0 {
break;
}
}
Ok(out)
}
/// Excel/Google Sheets compatible ROMAN(number, [form]) encoder.
/// `form`: 0..=4 (0=Classic, 4=Simplified).
pub fn to_roman_with_form(n: u32, form: i32) -> Result<String, String> {
let mut s = to_roman(n)?;
if form == 0 {
return Ok(s);
}
if !(0..=4).contains(&form) {
return Err("form must be between 0 and 4".into());
}
// Base rules (apply for all f >= 1)
let base_rules: &[(&str, &str)] = &[
// C(D|M)XC -> L$1XL
("CDXC", "LDXL"),
("CMXC", "LMXL"),
// C(D|M)L -> L$1
("CDL", "LD"),
("CML", "LM"),
// X(L|C)IX -> V$1IV
("XLIX", "VLIV"),
("XCIX", "VCIV"),
// X(L|C)V -> V$1
("XLV", "VL"),
("XCV", "VC"),
];
// Level 2 extra rules
let lvl2_rules: &[(&str, &str)] = &[
// V(L|C)IV -> I$1
("VLIV", "IL"),
("VCIV", "IC"),
// L(D|M)XL -> X$1
("LDXL", "XD"),
("LMXL", "XM"),
// L(D|M)VL -> X$1V
("LDVL", "XDV"),
("LMVL", "XMV"),
// L(D|M)IL -> X$1IX
("LDIL", "XDIX"),
("LMIL", "XMIX"),
];
// Level 3 extra rules
let lvl3_rules: &[(&str, &str)] = &[
// X(D|M)V -> V$1
("XDV", "VD"),
("XMV", "VM"),
// X(D|M)IX -> V$1IV
("XDIX", "VDIV"),
("XMIX", "VMIV"),
];
// Level 4 extra rules
let lvl4_rules: &[(&str, &str)] = &[
// V(D|M)IV -> I$1
("VDIV", "ID"),
("VMIV", "IM"),
];
// Helper to apply a batch of (from -> to) globally, in order.
fn apply_rules(mut t: String, rules: &[(&str, &str)]) -> String {
for (from, to) in rules {
if t.contains(from) {
t = t.replace(from, to);
}
}
t
}
s = apply_rules(s, base_rules);
if form >= 2 {
s = apply_rules(s, lvl2_rules);
}
if form >= 3 {
s = apply_rules(s, lvl3_rules);
}
if form >= 4 {
s = apply_rules(s, lvl4_rules);
}
Ok(s)
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ use crate::{
}; };
pub(crate) mod binary_search; pub(crate) mod binary_search;
mod database;
mod date_and_time; mod date_and_time;
mod engineering; mod engineering;
mod financial; mod financial;
@@ -17,7 +16,6 @@ mod information;
mod logical; mod logical;
mod lookup_and_reference; mod lookup_and_reference;
mod macros; mod macros;
mod math_util;
mod mathematical; mod mathematical;
mod statistical; mod statistical;
mod subtotal; mod subtotal;
@@ -78,43 +76,6 @@ pub enum Function {
Sumifs, Sumifs,
Tan, Tan,
Tanh, Tanh,
Acot,
Acoth,
Cot,
Coth,
Csc,
Csch,
Sec,
Sech,
Exp,
Fact,
Factdouble,
Sign,
Radians,
Degrees,
Int,
Even,
Odd,
Ceiling,
CeilingMath,
CeilingPrecise,
Floor,
FloorMath,
FloorPrecise,
IsoCeiling,
Mod,
Quotient,
Mround,
Trunc,
Gcd,
Lcm,
Base,
Decimal,
Roman,
Arabic,
Combin,
Combina,
Sumsq,
// Information // Information
ErrorType, ErrorType,
@@ -135,11 +96,6 @@ pub enum Function {
Sheet, Sheet,
Type, Type,
Sheets,
N,
Cell,
Info,
// Lookup and reference // Lookup and reference
Hlookup, Hlookup,
Index, Index,
@@ -192,30 +148,13 @@ pub enum Function {
// Date and time // Date and time
Date, Date,
Datedif,
Datevalue,
Day, Day,
Edate, Edate,
Eomonth, Eomonth,
Month, Month,
Time,
Timevalue,
Hour,
Minute,
Second,
Now, Now,
Today, Today,
Year, Year,
Networkdays,
NetworkdaysIntl,
Days,
Days360,
Weekday,
Weeknum,
Workday,
WorkdayIntl,
Yearfrac,
Isoweeknum,
// Financial // Financial
Cumipmt, Cumipmt,
@@ -311,24 +250,10 @@ pub enum Function {
Delta, Delta,
Gestep, Gestep,
Subtotal, Subtotal,
// Database
Daverage,
Dcount,
Dget,
Dmax,
Dmin,
Dsum,
Dcounta,
Dproduct,
Dstdev,
Dvar,
Dvarp,
Dstdevp,
} }
impl Function { impl Function {
pub fn into_iter() -> IntoIter<Function, 268> { pub fn into_iter() -> IntoIter<Function, 198> {
[ [
Function::And, Function::And,
Function::False, Function::False,
@@ -361,44 +286,12 @@ impl Function {
Function::Sqrt, Function::Sqrt,
Function::Sqrtpi, Function::Sqrtpi,
Function::Atan2, Function::Atan2,
Function::Acot,
Function::Acoth,
Function::Cot,
Function::Coth,
Function::Csc,
Function::Csch,
Function::Sec,
Function::Sech,
Function::Power, Function::Power,
Function::Exp,
Function::Fact,
Function::Factdouble,
Function::Sign,
Function::Int,
Function::Even,
Function::Odd,
Function::Ceiling,
Function::CeilingMath,
Function::CeilingPrecise,
Function::Floor,
Function::FloorMath,
Function::FloorPrecise,
Function::IsoCeiling,
Function::Mod,
Function::Quotient,
Function::Mround,
Function::Trunc,
Function::Gcd,
Function::Lcm,
Function::Base,
Function::Decimal,
Function::Max, Function::Max,
Function::Min, Function::Min,
Function::Product, Function::Product,
Function::Rand, Function::Rand,
Function::Randbetween, Function::Randbetween,
Function::Radians,
Function::Degrees,
Function::Round, Function::Round,
Function::Rounddown, Function::Rounddown,
Function::Roundup, Function::Roundup,
@@ -469,26 +362,9 @@ impl Function {
Function::Month, Function::Month,
Function::Eomonth, Function::Eomonth,
Function::Date, Function::Date,
Function::Datedif,
Function::Datevalue,
Function::Edate, Function::Edate,
Function::Networkdays,
Function::NetworkdaysIntl,
Function::Time,
Function::Timevalue,
Function::Hour,
Function::Minute,
Function::Second,
Function::Today, Function::Today,
Function::Now, Function::Now,
Function::Days,
Function::Days360,
Function::Weekday,
Function::Weeknum,
Function::Workday,
Function::WorkdayIntl,
Function::Yearfrac,
Function::Isoweeknum,
Function::Pmt, Function::Pmt,
Function::Pv, Function::Pv,
Function::Rate, Function::Rate,
@@ -577,27 +453,6 @@ impl Function {
Function::Delta, Function::Delta,
Function::Gestep, Function::Gestep,
Function::Subtotal, Function::Subtotal,
Function::Roman,
Function::Arabic,
Function::Combin,
Function::Combina,
Function::Sumsq,
Function::N,
Function::Cell,
Function::Info,
Function::Sheets,
Function::Daverage,
Function::Dcount,
Function::Dget,
Function::Dmax,
Function::Dmin,
Function::Dsum,
Function::Dcounta,
Function::Dproduct,
Function::Dstdev,
Function::Dvar,
Function::Dvarp,
Function::Dstdevp,
] ]
.into_iter() .into_iter()
} }
@@ -639,27 +494,6 @@ impl Function {
Function::Isformula => "_xlfn.ISFORMULA".to_string(), Function::Isformula => "_xlfn.ISFORMULA".to_string(),
Function::Sheet => "_xlfn.SHEET".to_string(), Function::Sheet => "_xlfn.SHEET".to_string(),
Function::Formulatext => "_xlfn.FORMULATEXT".to_string(), Function::Formulatext => "_xlfn.FORMULATEXT".to_string(),
Function::Isoweeknum => "_xlfn.ISOWEEKNUM".to_string(),
Function::Ceiling => "_xlfn.CEILING".to_string(),
Function::CeilingMath => "_xlfn.CEILING.MATH".to_string(),
Function::CeilingPrecise => "_xlfn.CEILING.PRECISE".to_string(),
Function::FloorMath => "_xlfn.FLOOR.MATH".to_string(),
Function::FloorPrecise => "_xlfn.FLOOR.PRECISE".to_string(),
Function::IsoCeiling => "_xlfn.ISO.CEILING".to_string(),
Function::Base => "_xlfn.BASE".to_string(),
Function::Decimal => "_xlfn.DECIMAL".to_string(),
Function::Arabic => "_xlfn.ARABIC".to_string(),
Function::Combina => "_xlfn.COMBINA".to_string(),
Function::Sheets => "_xlfn.SHEETS".to_string(),
Function::Acoth => "_xlfn.ACOTH".to_string(),
Function::Cot => "_xlfn.COT".to_string(),
Function::Coth => "_xlfn.COTH".to_string(),
Function::Csc => "_xlfn.CSC".to_string(),
Function::Csch => "_xlfn.CSCH".to_string(),
Function::Sec => "_xlfn.SEC".to_string(),
Function::Sech => "_xlfn.SECH".to_string(),
Function::Acot => "_xlfn.ACOT".to_string(),
_ => self.to_string(), _ => self.to_string(),
} }
} }
@@ -682,61 +516,34 @@ impl Function {
"SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch), "SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch),
"TRUE" => Some(Function::True), "TRUE" => Some(Function::True),
"XOR" | "_XLFN.XOR" => Some(Function::Xor), "XOR" | "_XLFN.XOR" => Some(Function::Xor),
"SIN" => Some(Function::Sin), "SIN" => Some(Function::Sin),
"COS" => Some(Function::Cos), "COS" => Some(Function::Cos),
"TAN" => Some(Function::Tan), "TAN" => Some(Function::Tan),
"ASIN" => Some(Function::Asin), "ASIN" => Some(Function::Asin),
"ACOS" => Some(Function::Acos), "ACOS" => Some(Function::Acos),
"ATAN" => Some(Function::Atan), "ATAN" => Some(Function::Atan),
"SINH" => Some(Function::Sinh), "SINH" => Some(Function::Sinh),
"COSH" => Some(Function::Cosh), "COSH" => Some(Function::Cosh),
"TANH" => Some(Function::Tanh), "TANH" => Some(Function::Tanh),
"ASINH" => Some(Function::Asinh), "ASINH" => Some(Function::Asinh),
"ACOSH" => Some(Function::Acosh), "ACOSH" => Some(Function::Acosh),
"ATANH" => Some(Function::Atanh), "ATANH" => Some(Function::Atanh),
"ACOT" | "_XLFN.ACOT" => Some(Function::Acot),
"COTH" | "_XLFN.COTH" => Some(Function::Coth),
"COT" | "_XLFN.COT" => Some(Function::Cot),
"CSC" | "_XLFN.CSC" => Some(Function::Csc),
"CSCH" | "_XLFN.CSCH" => Some(Function::Csch),
"SEC" | "_XLFN.SEC" => Some(Function::Sec),
"SECH" | "_XLFN.SECH" => Some(Function::Sech),
"ACOTH" | "_XLFN.ACOTH" => Some(Function::Acoth),
"FACT" => Some(Function::Fact),
"FACTDOUBLE" => Some(Function::Factdouble),
"EXP" => Some(Function::Exp),
"SIGN" => Some(Function::Sign),
"RADIANS" => Some(Function::Radians),
"DEGREES" => Some(Function::Degrees),
"INT" => Some(Function::Int),
"EVEN" => Some(Function::Even),
"ODD" => Some(Function::Odd),
"CEILING" | "_XLFN.CEILING" => Some(Function::Ceiling),
"CEILING.MATH" | "_XLFN.CEILING.MATH" => Some(Function::CeilingMath),
"CEILING.PRECISE" | "_XLFN.CEILING.PRECISE" => Some(Function::CeilingPrecise),
"FLOOR" => Some(Function::Floor),
"FLOOR.MATH" | "_XLFN.FLOOR.MATH" => Some(Function::FloorMath),
"FLOOR.PRECISE" | "_XLFN.FLOOR.PRECISE" => Some(Function::FloorPrecise),
"ISO.CEILING" | "_XLFN.ISO.CEILING" => Some(Function::IsoCeiling),
"MOD" => Some(Function::Mod),
"QUOTIENT" => Some(Function::Quotient),
"MROUND" => Some(Function::Mround),
"TRUNC" => Some(Function::Trunc),
"GCD" => Some(Function::Gcd),
"LCM" => Some(Function::Lcm),
"BASE" | "_XLFN.BASE" => Some(Function::Base),
"DECIMAL" | "_XLFN.DECIMAL" => Some(Function::Decimal),
"ROMAN" => Some(Function::Roman),
"ARABIC" | "_XLFN.ARABIC" => Some(Function::Arabic),
"PI" => Some(Function::Pi), "PI" => Some(Function::Pi),
"ABS" => Some(Function::Abs), "ABS" => Some(Function::Abs),
"SQRT" => Some(Function::Sqrt), "SQRT" => Some(Function::Sqrt),
"SQRTPI" => Some(Function::Sqrtpi), "SQRTPI" => Some(Function::Sqrtpi),
"POWER" => Some(Function::Power), "POWER" => Some(Function::Power),
"ATAN2" => Some(Function::Atan2), "ATAN2" => Some(Function::Atan2),
"LN" => Some(Function::Ln), "LN" => Some(Function::Ln),
"LOG" => Some(Function::Log), "LOG" => Some(Function::Log),
"LOG10" => Some(Function::Log10), "LOG10" => Some(Function::Log10),
"MAX" => Some(Function::Max), "MAX" => Some(Function::Max),
"MIN" => Some(Function::Min), "MIN" => Some(Function::Min),
"PRODUCT" => Some(Function::Product), "PRODUCT" => Some(Function::Product),
@@ -748,9 +555,6 @@ impl Function {
"SUM" => Some(Function::Sum), "SUM" => Some(Function::Sum),
"SUMIF" => Some(Function::Sumif), "SUMIF" => Some(Function::Sumif),
"SUMIFS" => Some(Function::Sumifs), "SUMIFS" => Some(Function::Sumifs),
"COMBIN" => Some(Function::Combin),
"COMBINA" | "_XLFN.COMBINA" => Some(Function::Combina),
"SUMSQ" => Some(Function::Sumsq),
// Lookup and Reference // Lookup and Reference
"CHOOSE" => Some(Function::Choose), "CHOOSE" => Some(Function::Choose),
@@ -827,26 +631,9 @@ impl Function {
"EOMONTH" => Some(Function::Eomonth), "EOMONTH" => Some(Function::Eomonth),
"MONTH" => Some(Function::Month), "MONTH" => Some(Function::Month),
"DATE" => Some(Function::Date), "DATE" => Some(Function::Date),
"DATEDIF" => Some(Function::Datedif),
"DATEVALUE" => Some(Function::Datevalue),
"EDATE" => Some(Function::Edate), "EDATE" => Some(Function::Edate),
"NETWORKDAYS" => Some(Function::Networkdays),
"NETWORKDAYS.INTL" => Some(Function::NetworkdaysIntl),
"TIME" => Some(Function::Time),
"TIMEVALUE" => Some(Function::Timevalue),
"HOUR" => Some(Function::Hour),
"MINUTE" => Some(Function::Minute),
"SECOND" => Some(Function::Second),
"TODAY" => Some(Function::Today), "TODAY" => Some(Function::Today),
"NOW" => Some(Function::Now), "NOW" => Some(Function::Now),
"DAYS" | "_XLFN.DAYS" => Some(Function::Days),
"DAYS360" => Some(Function::Days360),
"WEEKDAY" => Some(Function::Weekday),
"WEEKNUM" => Some(Function::Weeknum),
"WORKDAY" => Some(Function::Workday),
"WORKDAY.INTL" => Some(Function::WorkdayIntl),
"YEARFRAC" => Some(Function::Yearfrac),
"ISOWEEKNUM" | "_XLFN.ISOWEEKNUM" => Some(Function::Isoweeknum),
// Financial // Financial
"PMT" => Some(Function::Pmt), "PMT" => Some(Function::Pmt),
"PV" => Some(Function::Pv), "PV" => Some(Function::Pv),
@@ -938,25 +725,6 @@ impl Function {
"GESTEP" => Some(Function::Gestep), "GESTEP" => Some(Function::Gestep),
"SUBTOTAL" => Some(Function::Subtotal), "SUBTOTAL" => Some(Function::Subtotal),
"N" => Some(Function::N),
"CELL" => Some(Function::Cell),
"INFO" => Some(Function::Info),
"SHEETS" | "_XLFN.SHEETS" => Some(Function::Sheets),
"DAVERAGE" => Some(Function::Daverage),
"DCOUNT" => Some(Function::Dcount),
"DGET" => Some(Function::Dget),
"DMAX" => Some(Function::Dmax),
"DMIN" => Some(Function::Dmin),
"DSUM" => Some(Function::Dsum),
"DCOUNTA" => Some(Function::Dcounta),
"DPRODUCT" => Some(Function::Dproduct),
"DSTDEV" => Some(Function::Dstdev),
"DVAR" => Some(Function::Dvar),
"DVARP" => Some(Function::Dvarp),
"DSTDEVP" => Some(Function::Dstdevp),
_ => None, _ => None,
} }
} }
@@ -991,14 +759,6 @@ impl fmt::Display for Function {
Function::Asinh => write!(f, "ASINH"), Function::Asinh => write!(f, "ASINH"),
Function::Acosh => write!(f, "ACOSH"), Function::Acosh => write!(f, "ACOSH"),
Function::Atanh => write!(f, "ATANH"), Function::Atanh => write!(f, "ATANH"),
Function::Acot => write!(f, "ACOT"),
Function::Acoth => write!(f, "ACOTH"),
Function::Cot => write!(f, "COT"),
Function::Coth => write!(f, "COTH"),
Function::Csc => write!(f, "CSC"),
Function::Csch => write!(f, "CSCH"),
Function::Sec => write!(f, "SEC"),
Function::Sech => write!(f, "SECH"),
Function::Abs => write!(f, "ABS"), Function::Abs => write!(f, "ABS"),
Function::Pi => write!(f, "PI"), Function::Pi => write!(f, "PI"),
Function::Sqrt => write!(f, "SQRT"), Function::Sqrt => write!(f, "SQRT"),
@@ -1063,6 +823,7 @@ impl fmt::Display for Function {
Function::Isformula => write!(f, "ISFORMULA"), Function::Isformula => write!(f, "ISFORMULA"),
Function::Type => write!(f, "TYPE"), Function::Type => write!(f, "TYPE"),
Function::Sheet => write!(f, "SHEET"), Function::Sheet => write!(f, "SHEET"),
Function::Average => write!(f, "AVERAGE"), Function::Average => write!(f, "AVERAGE"),
Function::Averagea => write!(f, "AVERAGEA"), Function::Averagea => write!(f, "AVERAGEA"),
Function::Averageif => write!(f, "AVERAGEIF"), Function::Averageif => write!(f, "AVERAGEIF"),
@@ -1080,26 +841,9 @@ impl fmt::Display for Function {
Function::Month => write!(f, "MONTH"), Function::Month => write!(f, "MONTH"),
Function::Eomonth => write!(f, "EOMONTH"), Function::Eomonth => write!(f, "EOMONTH"),
Function::Date => write!(f, "DATE"), Function::Date => write!(f, "DATE"),
Function::Datedif => write!(f, "DATEDIF"),
Function::Datevalue => write!(f, "DATEVALUE"),
Function::Edate => write!(f, "EDATE"), Function::Edate => write!(f, "EDATE"),
Function::Networkdays => write!(f, "NETWORKDAYS"),
Function::NetworkdaysIntl => write!(f, "NETWORKDAYS.INTL"),
Function::Time => write!(f, "TIME"),
Function::Timevalue => write!(f, "TIMEVALUE"),
Function::Hour => write!(f, "HOUR"),
Function::Minute => write!(f, "MINUTE"),
Function::Second => write!(f, "SECOND"),
Function::Today => write!(f, "TODAY"), Function::Today => write!(f, "TODAY"),
Function::Now => write!(f, "NOW"), Function::Now => write!(f, "NOW"),
Function::Days => write!(f, "DAYS"),
Function::Days360 => write!(f, "DAYS360"),
Function::Weekday => write!(f, "WEEKDAY"),
Function::Weeknum => write!(f, "WEEKNUM"),
Function::Workday => write!(f, "WORKDAY"),
Function::WorkdayIntl => write!(f, "WORKDAY.INTL"),
Function::Yearfrac => write!(f, "YEARFRAC"),
Function::Isoweeknum => write!(f, "ISOWEEKNUM"),
Function::Pmt => write!(f, "PMT"), Function::Pmt => write!(f, "PMT"),
Function::Pv => write!(f, "PV"), Function::Pv => write!(f, "PV"),
Function::Rate => write!(f, "RATE"), Function::Rate => write!(f, "RATE"),
@@ -1187,53 +931,8 @@ impl fmt::Display for Function {
Function::Convert => write!(f, "CONVERT"), Function::Convert => write!(f, "CONVERT"),
Function::Delta => write!(f, "DELTA"), Function::Delta => write!(f, "DELTA"),
Function::Gestep => write!(f, "GESTEP"), Function::Gestep => write!(f, "GESTEP"),
Function::Subtotal => write!(f, "SUBTOTAL"),
Function::Exp => write!(f, "EXP"),
Function::Fact => write!(f, "FACT"),
Function::Factdouble => write!(f, "FACTDOUBLE"),
Function::Sign => write!(f, "SIGN"),
Function::Radians => write!(f, "RADIANS"),
Function::Degrees => write!(f, "DEGREES"),
Function::Int => write!(f, "INT"),
Function::Even => write!(f, "EVEN"),
Function::Odd => write!(f, "ODD"),
Function::Ceiling => write!(f, "CEILING"),
Function::CeilingMath => write!(f, "CEILING.MATH"),
Function::CeilingPrecise => write!(f, "CEILING.PRECISE"),
Function::Floor => write!(f, "FLOOR"),
Function::FloorMath => write!(f, "FLOOR.MATH"),
Function::FloorPrecise => write!(f, "FLOOR.PRECISE"),
Function::IsoCeiling => write!(f, "ISO.CEILING"),
Function::Mod => write!(f, "MOD"),
Function::Quotient => write!(f, "QUOTIENT"),
Function::Mround => write!(f, "MROUND"),
Function::Trunc => write!(f, "TRUNC"),
Function::Gcd => write!(f, "GCD"),
Function::Lcm => write!(f, "LCM"),
Function::Base => write!(f, "BASE"),
Function::Decimal => write!(f, "DECIMAL"),
Function::Roman => write!(f, "ROMAN"),
Function::Arabic => write!(f, "ARABIC"),
Function::Combin => write!(f, "COMBIN"),
Function::Combina => write!(f, "COMBINA"),
Function::Sumsq => write!(f, "SUMSQ"),
Function::N => write!(f, "N"), Function::Subtotal => write!(f, "SUBTOTAL"),
Function::Cell => write!(f, "CELL"),
Function::Info => write!(f, "INFO"),
Function::Sheets => write!(f, "SHEETS"),
Function::Daverage => write!(f, "DAVERAGE"),
Function::Dcount => write!(f, "DCOUNT"),
Function::Dget => write!(f, "DGET"),
Function::Dmax => write!(f, "DMAX"),
Function::Dmin => write!(f, "DMIN"),
Function::Dsum => write!(f, "DSUM"),
Function::Dcounta => write!(f, "DCOUNTA"),
Function::Dproduct => write!(f, "DPRODUCT"),
Function::Dstdev => write!(f, "DSTDEV"),
Function::Dvar => write!(f, "DVAR"),
Function::Dvarp => write!(f, "DVARP"),
Function::Dstdevp => write!(f, "DSTDEVP"),
} }
} }
} }
@@ -1262,6 +961,7 @@ impl Model {
cell: CellReferenceIndex, cell: CellReferenceIndex,
) -> CalcResult { ) -> CalcResult {
match kind { match kind {
// Logical
Function::And => self.fn_and(args, cell), Function::And => self.fn_and(args, cell),
Function::False => self.fn_false(args, cell), Function::False => self.fn_false(args, cell),
Function::If => self.fn_if(args, cell), Function::If => self.fn_if(args, cell),
@@ -1273,27 +973,34 @@ impl Model {
Function::Switch => self.fn_switch(args, cell), Function::Switch => self.fn_switch(args, cell),
Function::True => self.fn_true(args, cell), Function::True => self.fn_true(args, cell),
Function::Xor => self.fn_xor(args, cell), Function::Xor => self.fn_xor(args, cell),
// Math and trigonometry
Function::Log => self.fn_log(args, cell), Function::Log => self.fn_log(args, cell),
Function::Log10 => self.fn_log10(args, cell), Function::Log10 => self.fn_log10(args, cell),
Function::Ln => self.fn_ln(args, cell), Function::Ln => self.fn_ln(args, cell),
Function::Sin => self.fn_sin(args, cell), Function::Sin => self.fn_sin(args, cell),
Function::Cos => self.fn_cos(args, cell), Function::Cos => self.fn_cos(args, cell),
Function::Tan => self.fn_tan(args, cell), Function::Tan => self.fn_tan(args, cell),
Function::Asin => self.fn_asin(args, cell), Function::Asin => self.fn_asin(args, cell),
Function::Acos => self.fn_acos(args, cell), Function::Acos => self.fn_acos(args, cell),
Function::Atan => self.fn_atan(args, cell), Function::Atan => self.fn_atan(args, cell),
Function::Sinh => self.fn_sinh(args, cell), Function::Sinh => self.fn_sinh(args, cell),
Function::Cosh => self.fn_cosh(args, cell), Function::Cosh => self.fn_cosh(args, cell),
Function::Tanh => self.fn_tanh(args, cell), Function::Tanh => self.fn_tanh(args, cell),
Function::Asinh => self.fn_asinh(args, cell), Function::Asinh => self.fn_asinh(args, cell),
Function::Acosh => self.fn_acosh(args, cell), Function::Acosh => self.fn_acosh(args, cell),
Function::Atanh => self.fn_atanh(args, cell), Function::Atanh => self.fn_atanh(args, cell),
Function::Pi => self.fn_pi(args, cell), Function::Pi => self.fn_pi(args, cell),
Function::Abs => self.fn_abs(args, cell), Function::Abs => self.fn_abs(args, cell),
Function::Sqrt => self.fn_sqrt(args, cell), Function::Sqrt => self.fn_sqrt(args, cell),
Function::Sqrtpi => self.fn_sqrtpi(args, cell), Function::Sqrtpi => self.fn_sqrtpi(args, cell),
Function::Atan2 => self.fn_atan2(args, cell), Function::Atan2 => self.fn_atan2(args, cell),
Function::Power => self.fn_power(args, cell), Function::Power => self.fn_power(args, cell),
Function::Max => self.fn_max(args, cell), Function::Max => self.fn_max(args, cell),
Function::Min => self.fn_min(args, cell), Function::Min => self.fn_min(args, cell),
Function::Product => self.fn_product(args, cell), Function::Product => self.fn_product(args, cell),
@@ -1305,6 +1012,8 @@ impl Model {
Function::Sum => self.fn_sum(args, cell), Function::Sum => self.fn_sum(args, cell),
Function::Sumif => self.fn_sumif(args, cell), Function::Sumif => self.fn_sumif(args, cell),
Function::Sumifs => self.fn_sumifs(args, cell), Function::Sumifs => self.fn_sumifs(args, cell),
// Lookup and Reference
Function::Choose => self.fn_choose(args, cell), Function::Choose => self.fn_choose(args, cell),
Function::Column => self.fn_column(args, cell), Function::Column => self.fn_column(args, cell),
Function::Columns => self.fn_columns(args, cell), Function::Columns => self.fn_columns(args, cell),
@@ -1318,6 +1027,7 @@ impl Model {
Function::Rows => self.fn_rows(args, cell), Function::Rows => self.fn_rows(args, cell),
Function::Vlookup => self.fn_vlookup(args, cell), Function::Vlookup => self.fn_vlookup(args, cell),
Function::Xlookup => self.fn_xlookup(args, cell), Function::Xlookup => self.fn_xlookup(args, cell),
// Text
Function::Concatenate => self.fn_concatenate(args, cell), Function::Concatenate => self.fn_concatenate(args, cell),
Function::Exact => self.fn_exact(args, cell), Function::Exact => self.fn_exact(args, cell),
Function::Value => self.fn_value(args, cell), Function::Value => self.fn_value(args, cell),
@@ -1335,6 +1045,7 @@ impl Model {
Function::Trim => self.fn_trim(args, cell), Function::Trim => self.fn_trim(args, cell),
Function::Unicode => self.fn_unicode(args, cell), Function::Unicode => self.fn_unicode(args, cell),
Function::Upper => self.fn_upper(args, cell), Function::Upper => self.fn_upper(args, cell),
// Information
Function::Isnumber => self.fn_isnumber(args, cell), Function::Isnumber => self.fn_isnumber(args, cell),
Function::Isnontext => self.fn_isnontext(args, cell), Function::Isnontext => self.fn_isnontext(args, cell),
Function::Istext => self.fn_istext(args, cell), Function::Istext => self.fn_istext(args, cell),
@@ -1352,6 +1063,7 @@ impl Model {
Function::Isformula => self.fn_isformula(args, cell), Function::Isformula => self.fn_isformula(args, cell),
Function::Type => self.fn_type(args, cell), Function::Type => self.fn_type(args, cell),
Function::Sheet => self.fn_sheet(args, cell), Function::Sheet => self.fn_sheet(args, cell),
// Statistical
Function::Average => self.fn_average(args, cell), Function::Average => self.fn_average(args, cell),
Function::Averagea => self.fn_averagea(args, cell), Function::Averagea => self.fn_averagea(args, cell),
Function::Averageif => self.fn_averageif(args, cell), Function::Averageif => self.fn_averageif(args, cell),
@@ -1364,31 +1076,16 @@ impl Model {
Function::Maxifs => self.fn_maxifs(args, cell), Function::Maxifs => self.fn_maxifs(args, cell),
Function::Minifs => self.fn_minifs(args, cell), Function::Minifs => self.fn_minifs(args, cell),
Function::Geomean => self.fn_geomean(args, cell), Function::Geomean => self.fn_geomean(args, cell),
// Date and Time
Function::Year => self.fn_year(args, cell), Function::Year => self.fn_year(args, cell),
Function::Day => self.fn_day(args, cell), Function::Day => self.fn_day(args, cell),
Function::Eomonth => self.fn_eomonth(args, cell), Function::Eomonth => self.fn_eomonth(args, cell),
Function::Month => self.fn_month(args, cell), Function::Month => self.fn_month(args, cell),
Function::Date => self.fn_date(args, cell), Function::Date => self.fn_date(args, cell),
Function::Datedif => self.fn_datedif(args, cell),
Function::Datevalue => self.fn_datevalue(args, cell),
Function::Edate => self.fn_edate(args, cell), Function::Edate => self.fn_edate(args, cell),
Function::Networkdays => self.fn_networkdays(args, cell),
Function::NetworkdaysIntl => self.fn_networkdays_intl(args, cell),
Function::Time => self.fn_time(args, cell),
Function::Timevalue => self.fn_timevalue(args, cell),
Function::Hour => self.fn_hour(args, cell),
Function::Minute => self.fn_minute(args, cell),
Function::Second => self.fn_second(args, cell),
Function::Today => self.fn_today(args, cell), Function::Today => self.fn_today(args, cell),
Function::Now => self.fn_now(args, cell), Function::Now => self.fn_now(args, cell),
Function::Days => self.fn_days(args, cell), // Financial
Function::Days360 => self.fn_days360(args, cell),
Function::Weekday => self.fn_weekday(args, cell),
Function::Weeknum => self.fn_weeknum(args, cell),
Function::Workday => self.fn_workday(args, cell),
Function::WorkdayIntl => self.fn_workday_intl(args, cell),
Function::Yearfrac => self.fn_yearfrac(args, cell),
Function::Isoweeknum => self.fn_isoweeknum(args, cell),
Function::Pmt => self.fn_pmt(args, cell), Function::Pmt => self.fn_pmt(args, cell),
Function::Pv => self.fn_pv(args, cell), Function::Pv => self.fn_pv(args, cell),
Function::Rate => self.fn_rate(args, cell), Function::Rate => self.fn_rate(args, cell),
@@ -1422,6 +1119,7 @@ impl Model {
Function::Db => self.fn_db(args, cell), Function::Db => self.fn_db(args, cell),
Function::Cumprinc => self.fn_cumprinc(args, cell), Function::Cumprinc => self.fn_cumprinc(args, cell),
Function::Cumipmt => self.fn_cumipmt(args, cell), Function::Cumipmt => self.fn_cumipmt(args, cell),
// Engineering
Function::Besseli => self.fn_besseli(args, cell), Function::Besseli => self.fn_besseli(args, cell),
Function::Besselj => self.fn_besselj(args, cell), Function::Besselj => self.fn_besselj(args, cell),
Function::Besselk => self.fn_besselk(args, cell), Function::Besselk => self.fn_besselk(args, cell),
@@ -1476,60 +1174,8 @@ impl Model {
Function::Convert => self.fn_convert(args, cell), Function::Convert => self.fn_convert(args, cell),
Function::Delta => self.fn_delta(args, cell), Function::Delta => self.fn_delta(args, cell),
Function::Gestep => self.fn_gestep(args, cell), Function::Gestep => self.fn_gestep(args, cell),
Function::Subtotal => self.fn_subtotal(args, cell), Function::Subtotal => self.fn_subtotal(args, cell),
Function::Acot => self.fn_acot(args, cell),
Function::Acoth => self.fn_acoth(args, cell),
Function::Cot => self.fn_cot(args, cell),
Function::Coth => self.fn_coth(args, cell),
Function::Csc => self.fn_csc(args, cell),
Function::Csch => self.fn_csch(args, cell),
Function::Sec => self.fn_sec(args, cell),
Function::Sech => self.fn_sech(args, cell),
Function::Exp => self.fn_exp(args, cell),
Function::Fact => self.fn_fact(args, cell),
Function::Factdouble => self.fn_factdouble(args, cell),
Function::Sign => self.fn_sign(args, cell),
Function::Radians => self.fn_radians(args, cell),
Function::Degrees => self.fn_degrees(args, cell),
Function::Int => self.fn_int(args, cell),
Function::Even => self.fn_even(args, cell),
Function::Odd => self.fn_odd(args, cell),
Function::Ceiling => self.fn_ceiling(args, cell),
Function::CeilingMath => self.fn_ceiling_math(args, cell),
Function::CeilingPrecise => self.fn_ceiling_precise(args, cell),
Function::Floor => self.fn_floor(args, cell),
Function::FloorMath => self.fn_floor_math(args, cell),
Function::FloorPrecise => self.fn_floor_precise(args, cell),
Function::IsoCeiling => self.fn_iso_ceiling(args, cell),
Function::Mod => self.fn_mod(args, cell),
Function::Quotient => self.fn_quotient(args, cell),
Function::Mround => self.fn_mround(args, cell),
Function::Trunc => self.fn_trunc(args, cell),
Function::Gcd => self.fn_gcd(args, cell),
Function::Lcm => self.fn_lcm(args, cell),
Function::Base => self.fn_base(args, cell),
Function::Decimal => self.fn_decimal(args, cell),
Function::Roman => self.fn_roman(args, cell),
Function::Arabic => self.fn_arabic(args, cell),
Function::Combin => self.fn_combin(args, cell),
Function::Combina => self.fn_combina(args, cell),
Function::Sumsq => self.fn_sumsq(args, cell),
Function::N => self.fn_n(args, cell),
Function::Cell => self.fn_cell(args, cell),
Function::Info => self.fn_info(args, cell),
Function::Sheets => self.fn_sheets(args, cell),
Function::Daverage => self.fn_daverage(args, cell),
Function::Dcount => self.fn_dcount(args, cell),
Function::Dget => self.fn_dget(args, cell),
Function::Dmax => self.fn_dmax(args, cell),
Function::Dmin => self.fn_dmin(args, cell),
Function::Dsum => self.fn_dsum(args, cell),
Function::Dcounta => self.fn_dcounta(args, cell),
Function::Dproduct => self.fn_dproduct(args, cell),
Function::Dstdev => self.fn_dstdev(args, cell),
Function::Dvar => self.fn_dvar(args, cell),
Function::Dvarp => self.fn_dvarp(args, cell),
Function::Dstdevp => self.fn_dstdevp(args, cell),
} }
} }
} }

View File

@@ -1,10 +1,7 @@
#[cfg(feature = "use_regex_lite")] #[cfg(feature = "use_regex_lite")]
use regex_lite as regex; use regex_lite as regex;
use crate::{ use crate::{calc_result::CalcResult, expressions::token::is_english_error_string};
calc_result::CalcResult, expressions::token::is_english_error_string,
number_format::to_excel_precision,
};
/// This test for exact match (modulo case). /// This test for exact match (modulo case).
/// * strings are not cast into bools or numbers /// * strings are not cast into bools or numbers
@@ -37,8 +34,6 @@ pub(crate) fn values_are_equal(left: &CalcResult, right: &CalcResult) -> bool {
pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 { pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
match (left, right) { match (left, right) {
(CalcResult::Number(value1), CalcResult::Number(value2)) => { (CalcResult::Number(value1), CalcResult::Number(value2)) => {
let value1 = to_excel_precision(*value1, 15);
let value2 = to_excel_precision(*value2, 15);
if (value2 - value1).abs() < f64::EPSILON { if (value2 - value1).abs() < f64::EPSILON {
return 0; return 0;
} }

View File

@@ -2068,7 +2068,21 @@ impl Model {
scope: Option<u32>, scope: Option<u32>,
formula: &str, formula: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let sheet_id = self.is_valid_defined_name(name, scope, formula)?; if !is_valid_identifier(name) {
return Err("Invalid defined name".to_string());
};
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
None => None,
};
// if the defined name already exist return error
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Err("Defined name already exists".to_string());
}
}
self.workbook.defined_names.push(DefinedName { self.workbook.defined_names.push(DefinedName {
name: name.to_string(), name: name.to_string(),
formula: formula.to_string(), formula: formula.to_string(),
@@ -2079,48 +2093,6 @@ impl Model {
Ok(()) Ok(())
} }
/// Validates if a defined name can be created
pub fn is_valid_defined_name(
&self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<Option<u32>, String> {
if !is_valid_identifier(name) {
return Err("Name: Invalid defined name".to_string());
}
let name_upper = name.to_uppercase();
let defined_names = &self.workbook.defined_names;
let sheet_id = match scope {
Some(index) => match self.workbook.worksheet(index) {
Ok(ws) => Some(ws.sheet_id),
Err(_) => return Err("Scope: Invalid sheet index".to_string()),
},
None => None,
};
// if the defined name already exist return error
for df in defined_names {
if df.name.to_uppercase() == name_upper && df.sheet_id == sheet_id {
return Err("Name: Defined name already exists".to_string());
}
}
// Make sure the formula is valid
match common::ParsedReference::parse_reference_formula(
None,
formula,
&self.locale,
|name| self.get_sheet_index_by_name(name),
) {
Ok(_) => {}
Err(_) => {
return Err("Formula: Invalid defined name formula".to_string());
}
};
Ok(sheet_id)
}
/// Delete defined name of name and scope /// Delete defined name of name and scope
pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> { pub fn delete_defined_name(&mut self, name: &str, scope: Option<u32>) -> Result<(), String> {
let name_upper = name.to_uppercase(); let name_upper = name.to_uppercase();
@@ -2154,7 +2126,7 @@ impl Model {
new_formula: &str, new_formula: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if !is_valid_identifier(new_name) { if !is_valid_identifier(new_name) {
return Err("Name: Invalid defined name".to_string()); return Err("Invalid defined name".to_string());
}; };
let name_upper = name.to_uppercase(); let name_upper = name.to_uppercase();
let new_name_upper = new_name.to_uppercase(); let new_name_upper = new_name.to_uppercase();
@@ -2162,28 +2134,18 @@ impl Model {
if name_upper != new_name_upper || scope != new_scope { if name_upper != new_name_upper || scope != new_scope {
for key in self.parsed_defined_names.keys() { for key in self.parsed_defined_names.keys() {
if key.1.to_uppercase() == new_name_upper && key.0 == new_scope { if key.1.to_uppercase() == new_name_upper && key.0 == new_scope {
return Err("Name: Defined name already exists".to_string()); return Err("Defined name already exists".to_string());
} }
} }
} }
let defined_names = &self.workbook.defined_names; let defined_names = &self.workbook.defined_names;
let sheet_id = match scope { let sheet_id = match scope {
Some(index) => Some( Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
self.workbook
.worksheet(index)
.map_err(|_| "Scope: Invalid sheet index")?
.sheet_id,
),
None => None, None => None,
}; };
let new_sheet_id = match new_scope { let new_sheet_id = match new_scope {
Some(index) => Some( Some(index) => Some(self.workbook.worksheet(index)?.sheet_id),
self.workbook
.worksheet(index)
.map_err(|_| "Scope: Invalid sheet index")?
.sheet_id,
),
None => None, None => None,
}; };

View File

@@ -112,36 +112,29 @@ pub fn to_precision(value: f64, precision: usize) -> f64 {
/// ``` /// ```
/// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})` /// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})`
/// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)). /// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)).
/// FIXME: There has to be a better algorithm :/
pub fn to_excel_precision_str(value: f64) -> String { pub fn to_excel_precision_str(value: f64) -> String {
to_precision_str(value, 15) to_precision_str(value, 15)
} }
pub fn to_excel_precision(value: f64, precision: usize) -> f64 {
if !value.is_finite() {
return value;
}
let s = format!("{:.*e}", precision.saturating_sub(1), value);
s.parse::<f64>().unwrap_or(value)
}
pub fn to_precision_str(value: f64, precision: usize) -> String { pub fn to_precision_str(value: f64, precision: usize) -> String {
if !value.is_finite() { if value.is_infinite() {
if value.is_infinite() { return "inf".to_string();
return "inf".to_string();
} else {
return "NaN".to_string();
}
} }
if value.is_nan() {
let s = format!("{:.*e}", precision.saturating_sub(1), value); return "NaN".to_string();
let parsed = s.parse::<f64>().unwrap_or(value); }
let exponent = value.abs().log10().floor();
let base = value / 10.0_f64.powf(exponent);
let base = format!("{0:.1$}", base, precision - 1);
let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
// TODO: do this in a way that does not require a possible error
0.0
});
// I would love to use the std library. There is not a speed concern here // I would love to use the std library. There is not a speed concern here
// problem is it doesn't do the right thing // problem is it doesn't do the right thing
// Also ryu is my favorite _modern_ algorithm // Also ryu is my favorite _modern_ algorithm
let mut buffer = ryu::Buffer::new(); let mut buffer = ryu::Buffer::new();
let text = buffer.format(parsed); let text = buffer.format(value);
// The above algorithm converts 2 to 2.0 regrettably // The above algorithm converts 2 to 2.0 regrettably
if let Some(stripped) = text.strip_suffix(".0") { if let Some(stripped) = text.strip_suffix(".0") {
return stripped.to_string(); return stripped.to_string();

View File

@@ -133,7 +133,6 @@ fn fn_imcot() {
); );
} }
#[cfg_attr(target_os = "windows", ignore)]
#[test] #[test]
fn fn_imtan() { fn fn_imtan() {
let mut model = new_empty_model(); let mut model = new_empty_model();

View File

@@ -7,9 +7,6 @@ mod test_column_width;
mod test_criteria; mod test_criteria;
mod test_currency; mod test_currency;
mod test_date_and_time; mod test_date_and_time;
mod test_datedif_leap_month_end;
mod test_days360_month_end;
mod test_degrees_radians;
mod test_error_propagation; mod test_error_propagation;
mod test_fn_average; mod test_fn_average;
mod test_fn_averageifs; mod test_fn_averageifs;
@@ -30,7 +27,6 @@ mod test_fn_sum;
mod test_fn_sumifs; mod test_fn_sumifs;
mod test_fn_textbefore; mod test_fn_textbefore;
mod test_fn_textjoin; mod test_fn_textjoin;
mod test_fn_time;
mod test_fn_unicode; mod test_fn_unicode;
mod test_frozen_rows_columns; mod test_frozen_rows_columns;
mod test_general; mod test_general;
@@ -47,11 +43,8 @@ mod test_sheets;
mod test_styles; mod test_styles;
mod test_trigonometric; mod test_trigonometric;
mod test_true_false; mod test_true_false;
mod test_weekday_return_types;
mod test_weeknum_return_types;
mod test_workbook; mod test_workbook;
mod test_worksheet; mod test_worksheet;
mod test_yearfrac_basis;
pub(crate) mod util; pub(crate) mod util;
mod engineering; mod engineering;
@@ -69,16 +62,11 @@ mod test_geomean;
mod test_get_cell_content; mod test_get_cell_content;
mod test_implicit_intersection; mod test_implicit_intersection;
mod test_issue_155; mod test_issue_155;
mod test_issue_483;
mod test_ln; mod test_ln;
mod test_log; mod test_log;
mod test_log10; mod test_log10;
mod test_mod_quotient;
mod test_networkdays;
mod test_now;
mod test_percentage; mod test_percentage;
mod test_set_functions_error_handling; mod test_set_functions_error_handling;
mod test_sheet_names;
mod test_today; mod test_today;
mod test_types; mod test_types;
mod user_model; mod user_model;

View File

@@ -1,27 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=ARABIC()");
model._set("A2", "=ARABIC(V)");
model._set("A3", "=ARABIC(V, 2)");
model._set("A4", "=ROMAN()");
model._set("A5", "=ROMAN(5)");
model._set("A6", "=ROMAN(5, 0)");
model._set("A7", "=ROMAN(5, 0, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"5");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"V");
assert_eq!(model._get_text("A6"), *"V");
assert_eq!(model._get_text("A7"), *"#ERROR!");
}

View File

@@ -1,27 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=COMBIN(5,2)");
model._set("A2", "=COMBINA(5,2)");
model._set("A3", "=COMBIN()");
model._set("A4", "=COMBINA()");
model._set("A5", "=COMBIN(2)");
model._set("A6", "=COMBINA(2)");
model._set("A5", "=COMBIN(1, 2, 3)");
model._set("A6", "=COMBINA(1, 2, 3)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"10");
assert_eq!(model._get_text("A2"), *"15");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#ERROR!");
}

View File

@@ -6,11 +6,6 @@
/// We can also enter examples that illustrate/document a part of the function /// We can also enter examples that illustrate/document a part of the function
use crate::{cell::CellValue, test::util::new_empty_model}; use crate::{cell::CellValue, test::util::new_empty_model};
// Excel uses a serial date system where Jan 1, 1900 = 1 (though it treats 1900 as a leap year)
// Most test dates are documented inline, but we define boundary values here:
const EXCEL_MAX_DATE: f64 = 2958465.0; // Dec 31, 9999 - used in boundary tests
const EXCEL_INVALID_DATE: f64 = 2958466.0; // One day past max - used in error tests
#[test] #[test]
fn test_fn_date_arguments() { fn test_fn_date_arguments() {
let mut model = new_empty_model(); let mut model = new_empty_model();
@@ -221,380 +216,3 @@ fn test_date_early_dates() {
Ok(CellValue::Number(61.0)) Ok(CellValue::Number(61.0))
); );
} }
#[test]
fn test_days_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=DAYS(44570,44561)");
model._set("A2", "=DAYS(44561,44570)"); // Reversed order
model._set("A3", "=DAYS(44561,44561)");
// Edge cases
model._set("A4", "=DAYS(1,2)"); // Early dates
model._set(
"A5",
&format!("=DAYS({},{})", EXCEL_MAX_DATE, EXCEL_MAX_DATE - 1.0),
); // Near max date
// Error cases - wrong argument count
model._set("A6", "=DAYS()");
model._set("A7", "=DAYS(44561)");
model._set("A8", "=DAYS(44561,44570,1)");
// Error cases - invalid dates
model._set("A9", "=DAYS(-1,44561)");
model._set("A10", &format!("=DAYS(44561,{EXCEL_INVALID_DATE})"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"9");
assert_eq!(model._get_text("A2"), *"-9");
assert_eq!(model._get_text("A3"), *"0");
assert_eq!(model._get_text("A4"), *"-1"); // DAYS(1,2) = 1-2 = -1
assert_eq!(model._get_text("A5"), *"1");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#ERROR!");
assert_eq!(model._get_text("A9"), *"#NUM!");
assert_eq!(model._get_text("A10"), *"#NUM!");
}
#[test]
fn test_days360_function() {
let mut model = new_empty_model();
// Basic functionality with different basis values
model._set("A1", "=DAYS360(44196,44560)"); // Default basis (US 30/360)
model._set("A2", "=DAYS360(44196,44560,FALSE)"); // US 30/360 explicitly
model._set("A3", "=DAYS360(44196,44560,TRUE)"); // European 30/360
// Same date
model._set("A4", "=DAYS360(44561,44561)");
model._set("A5", "=DAYS360(44561,44561,TRUE)");
// Reverse order (negative result)
model._set("A6", "=DAYS360(44560,44196)");
model._set("A7", "=DAYS360(44560,44196,TRUE)");
// Edge cases
model._set("A8", "=DAYS360(1,2)");
model._set("A9", "=DAYS360(1,2,FALSE)");
// Error cases - wrong argument count
model._set("A10", "=DAYS360()");
model._set("A11", "=DAYS360(44561)");
model._set("A12", "=DAYS360(44561,44570,TRUE,1)");
// Error cases - invalid dates
model._set("A13", "=DAYS360(-1,44561)");
model._set("A14", &format!("=DAYS360(44561,{EXCEL_INVALID_DATE})"));
model.evaluate();
assert_eq!(model._get_text("A1"), *"360");
assert_eq!(model._get_text("A2"), *"360");
assert_eq!(model._get_text("A3"), *"360");
assert_eq!(model._get_text("A4"), *"0");
assert_eq!(model._get_text("A5"), *"0");
assert_eq!(model._get_text("A6"), *"-360");
assert_eq!(model._get_text("A7"), *"-360");
assert_eq!(model._get_text("A8"), *"1");
assert_eq!(model._get_text("A9"), *"1");
assert_eq!(model._get_text("A10"), *"#ERROR!");
assert_eq!(model._get_text("A11"), *"#ERROR!");
assert_eq!(model._get_text("A12"), *"#ERROR!");
assert_eq!(model._get_text("A13"), *"#NUM!");
assert_eq!(model._get_text("A14"), *"#NUM!");
}
#[test]
fn test_weekday_function() {
let mut model = new_empty_model();
// Test return_type parameter variations with one known date (Friday 44561)
model._set("A1", "=WEEKDAY(44561)"); // Default: Sun=1, Fri=6
model._set("A2", "=WEEKDAY(44561,2)"); // Mon=1, Fri=5
model._set("A3", "=WEEKDAY(44561,3)"); // Mon=0, Fri=4
// Test boundary days (Sun/Mon) to verify return_type logic
model._set("A4", "=WEEKDAY(44556,1)"); // Sunday: should be 1
model._set("A5", "=WEEKDAY(44556,2)"); // Sunday: should be 7
model._set("A6", "=WEEKDAY(44557,2)"); // Monday: should be 1
// Error cases
model._set("A7", "=WEEKDAY()"); // Wrong arg count
model._set("A8", "=WEEKDAY(44561,0)"); // Invalid return_type
model._set("A9", "=WEEKDAY(-1)"); // Invalid date
model.evaluate();
// Core functionality
assert_eq!(model._get_text("A1"), *"6"); // Friday default
assert_eq!(model._get_text("A2"), *"5"); // Friday Mon=1
assert_eq!(model._get_text("A3"), *"4"); // Friday Mon=0
// Boundary verification
assert_eq!(model._get_text("A4"), *"1"); // Sunday Sun=1
assert_eq!(model._get_text("A5"), *"7"); // Sunday Mon=1
assert_eq!(model._get_text("A6"), *"1"); // Monday Mon=1
// Error cases
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#VALUE!");
assert_eq!(model._get_text("A9"), *"#NUM!");
}
#[test]
fn test_weeknum_function() {
let mut model = new_empty_model();
// Test different return_type values (1=week starts Sunday, 2=week starts Monday)
model._set("A1", "=WEEKNUM(44561)"); // Default return_type=1
model._set("A2", "=WEEKNUM(44561,1)"); // Sunday start
model._set("A3", "=WEEKNUM(44561,2)"); // Monday start
// Test year boundaries
model._set("A4", "=WEEKNUM(43831,1)"); // Jan 1, 2020 (Wednesday)
model._set("A5", "=WEEKNUM(43831,2)"); // Jan 1, 2020 (Wednesday)
model._set("A6", "=WEEKNUM(44196,1)"); // Dec 31, 2020 (Thursday)
model._set("A7", "=WEEKNUM(44196,2)"); // Dec 31, 2020 (Thursday)
// Test first and last weeks of year
model._set("A8", "=WEEKNUM(44197,1)"); // Jan 1, 2021 (Friday)
model._set("A9", "=WEEKNUM(44197,2)"); // Jan 1, 2021 (Friday)
model._set("A10", "=WEEKNUM(44561,1)"); // Dec 31, 2021 (Friday)
model._set("A11", "=WEEKNUM(44561,2)"); // Dec 31, 2021 (Friday)
// Error cases - wrong argument count
model._set("A12", "=WEEKNUM()");
model._set("A13", "=WEEKNUM(44561,1,1)");
// Error cases - invalid return_type
model._set("A14", "=WEEKNUM(44561,0)");
model._set("A15", "=WEEKNUM(44561,3)");
model._set("A16", "=WEEKNUM(44561,-1)");
// Error cases - invalid dates
model._set("A17", "=WEEKNUM(-1)");
model._set("A18", &format!("=WEEKNUM({EXCEL_INVALID_DATE})"));
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"53"); // Week 53
assert_eq!(model._get_text("A2"), *"53"); // Week 53 (Sunday start)
assert_eq!(model._get_text("A3"), *"53"); // Week 53 (Monday start)
// Year boundary tests
assert_eq!(model._get_text("A4"), *"1"); // Jan 1, 2020 (Sunday start)
assert_eq!(model._get_text("A5"), *"1"); // Jan 1, 2020 (Monday start)
assert_eq!(model._get_text("A6"), *"53"); // Dec 31, 2020 (Sunday start)
assert_eq!(model._get_text("A7"), *"53"); // Dec 31, 2020 (Monday start)
// 2021 tests
assert_eq!(model._get_text("A8"), *"1"); // Jan 1, 2021 (Sunday start)
assert_eq!(model._get_text("A9"), *"1"); // Jan 1, 2021 (Monday start)
assert_eq!(model._get_text("A10"), *"53"); // Dec 31, 2021 (Sunday start)
assert_eq!(model._get_text("A11"), *"53"); // Dec 31, 2021 (Monday start)
// Error cases
assert_eq!(model._get_text("A12"), *"#ERROR!");
assert_eq!(model._get_text("A13"), *"#ERROR!");
assert_eq!(model._get_text("A14"), *"#VALUE!");
assert_eq!(model._get_text("A15"), *"#VALUE!");
assert_eq!(model._get_text("A16"), *"#VALUE!");
assert_eq!(model._get_text("A17"), *"#NUM!");
assert_eq!(model._get_text("A18"), *"#NUM!");
}
#[test]
fn test_workday_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=WORKDAY(44560,1)");
model._set("A2", "=WORKDAY(44561,-1)");
model._set("A3", "=WORKDAY(44561,0)");
model._set("A4", "=WORKDAY(44560,5)");
// Test with holidays
model._set("B1", "44561");
model._set("A5", "=WORKDAY(44560,1,B1)"); // Should skip the holiday
model._set("B2", "44562");
model._set("B3", "44563");
model._set("A6", "=WORKDAY(44560,3,B1:B3)"); // Multiple holidays
// Test starting on weekend
model._set("A7", "=WORKDAY(44562,1)"); // Saturday start
model._set("A8", "=WORKDAY(44563,1)"); // Sunday start
// Test negative workdays
model._set("A9", "=WORKDAY(44565,-3)"); // Go backwards 3 days
model._set("A10", "=WORKDAY(44565,-5,B1:B3)"); // Backwards with holidays
// Edge cases
model._set("A11", "=WORKDAY(1,1)"); // Early date
model._set("A12", "=WORKDAY(100000,10)"); // Large numbers
// Error cases - wrong argument count
model._set("A13", "=WORKDAY()");
model._set("A14", "=WORKDAY(44560)");
model._set("A15", "=WORKDAY(44560,1,B1,B2)");
// Error cases - invalid dates
model._set("A16", "=WORKDAY(-1,1)");
model._set("A17", &format!("=WORKDAY({EXCEL_INVALID_DATE},1)"));
// Error cases - invalid holiday dates
model._set("B4", "-1");
model._set("A18", "=WORKDAY(44560,1,B4)");
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"44561"); // 1 day forward
assert_eq!(model._get_text("A2"), *"44560"); // 1 day backward
assert_eq!(model._get_text("A3"), *"44561"); // 0 days
assert_eq!(model._get_text("A4"), *"44567"); // 5 days forward
// With holidays
assert_eq!(model._get_text("A5"), *"44564"); // Skip holiday, go to Monday
assert_eq!(model._get_text("A6"), *"44566"); // Skip multiple holidays
// Weekend starts
assert_eq!(model._get_text("A7"), *"44564"); // From Saturday
assert_eq!(model._get_text("A8"), *"44564"); // From Sunday
// Negative workdays
assert_eq!(model._get_text("A9"), *"44560"); // 3 days back
assert_eq!(model._get_text("A10"), *"44557"); // 5 days back with holidays
// Edge cases
assert_eq!(model._get_text("A11"), *"2"); // Early date
assert_eq!(model._get_text("A12"), *"100014"); // Large numbers
// Error cases
assert_eq!(model._get_text("A13"), *"#ERROR!");
assert_eq!(model._get_text("A14"), *"#ERROR!");
assert_eq!(model._get_text("A15"), *"#ERROR!");
assert_eq!(model._get_text("A16"), *"#NUM!");
assert_eq!(model._get_text("A17"), *"#NUM!");
assert_eq!(model._get_text("A18"), *"#NUM!"); // Invalid holiday
}
#[test]
fn test_workday_intl_function() {
let mut model = new_empty_model();
// Test key weekend mask types
model._set("A1", "=WORKDAY.INTL(44560,1,1)"); // Numeric: standard (Sat-Sun)
model._set("A2", "=WORKDAY.INTL(44560,1,2)"); // Numeric: Sun-Mon
model._set("A3", "=WORKDAY.INTL(44560,1,\"0000001\")"); // String: Sunday only
model._set("A4", "=WORKDAY.INTL(44560,1,\"1100000\")"); // String: Mon-Tue
// Test with holidays
model._set("B1", "44561");
model._set("A5", "=WORKDAY.INTL(44560,2,1,B1)"); // Standard + holiday
model._set("A6", "=WORKDAY.INTL(44560,2,7,B1)"); // Fri-Sat + holiday
// Basic edge cases
model._set("A7", "=WORKDAY.INTL(44561,0,1)"); // Zero days
model._set("A8", "=WORKDAY.INTL(44565,-1,1)"); // Negative days
// Error cases
model._set("A9", "=WORKDAY.INTL()"); // Wrong arg count
model._set("A10", "=WORKDAY.INTL(44560,1,0)"); // Invalid weekend mask
model._set("A11", "=WORKDAY.INTL(44560,1,\"123\")"); // Invalid string mask
model._set("A12", "=WORKDAY.INTL(-1,1,1)"); // Invalid date
model.evaluate();
// Weekend mask functionality
assert_eq!(model._get_text("A1"), *"44561"); // Standard weekend
assert_eq!(model._get_text("A2"), *"44561"); // Sun-Mon weekend
assert_eq!(model._get_text("A3"), *"44561"); // Sunday only
assert_eq!(model._get_text("A4"), *"44561"); // Mon-Tue weekend
// With holidays
assert_eq!(model._get_text("A5"), *"44565"); // Skip holiday + standard weekend
assert_eq!(model._get_text("A6"), *"44564"); // Skip holiday + Fri-Sat weekend
// Edge cases
assert_eq!(model._get_text("A7"), *"44561"); // Zero days
assert_eq!(model._get_text("A8"), *"44564"); // Negative days
// Error cases
assert_eq!(model._get_text("A9"), *"#ERROR!");
assert_eq!(model._get_text("A10"), *"#NUM!");
assert_eq!(model._get_text("A11"), *"#VALUE!");
assert_eq!(model._get_text("A12"), *"#NUM!");
}
#[test]
fn test_yearfrac_function() {
let mut model = new_empty_model();
// Test key basis values (not exhaustive - just verify parameter works)
model._set("A1", "=YEARFRAC(44561,44926)"); // Default (30/360)
model._set("A2", "=YEARFRAC(44561,44926,1)"); // Actual/actual
model._set("A3", "=YEARFRAC(44561,44926,4)"); // European 30/360
// Edge cases
model._set("A4", "=YEARFRAC(44561,44561,1)"); // Same date = 0
model._set("A6", "=YEARFRAC(44197,44562,1)"); // Exact year (2021)
// Error cases
model._set("A7", "=YEARFRAC()"); // Wrong arg count
model._set("A8", "=YEARFRAC(44561,44926,5)"); // Invalid basis
model._set("A9", "=YEARFRAC(-1,44926,1)"); // Invalid date
model.evaluate();
// Basic functionality (approximate values expected)
assert_eq!(model._get_text("A1"), *"1"); // About 1 year
assert_eq!(model._get_text("A2"), *"1"); // About 1 year
assert_eq!(model._get_text("A3"), *"1"); // About 1 year
// Edge cases
assert_eq!(model._get_text("A4"), *"0"); // Same date
assert_eq!(model._get_text("A6"), *"1"); // Exact year
// Error cases
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#NUM!"); // Invalid basis should return #NUM!
assert_eq!(model._get_text("A9"), *"#NUM!");
}
#[test]
fn test_isoweeknum_function() {
let mut model = new_empty_model();
// Basic functionality
model._set("A1", "=ISOWEEKNUM(44563)"); // Mid-week date
model._set("A2", "=ISOWEEKNUM(44561)"); // Year-end date
// Key ISO week boundaries (just critical cases)
model._set("A3", "=ISOWEEKNUM(44197)"); // Jan 1, 2021 (Fri) -> Week 53 of 2020
model._set("A4", "=ISOWEEKNUM(44200)"); // Jan 4, 2021 (Mon) -> Week 1 of 2021
model._set("A5", "=ISOWEEKNUM(44564)"); // Jan 3, 2022 (Mon) -> Week 1 of 2022
// Error cases
model._set("A6", "=ISOWEEKNUM()"); // Wrong arg count
model._set("A7", "=ISOWEEKNUM(-1)"); // Invalid date
model.evaluate();
// Basic functionality
assert_eq!(model._get_text("A1"), *"52");
assert_eq!(model._get_text("A2"), *"52");
// ISO week boundaries
assert_eq!(model._get_text("A3"), *"53"); // Week 53 of previous year
assert_eq!(model._get_text("A4"), *"1"); // Week 1 of current year
assert_eq!(model._get_text("A5"), *"1"); // Week 1 of next year
// Error cases
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#NUM!");
}

View File

@@ -1,33 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_datedif_yd_leap_year_edge_cases() {
let mut model = new_empty_model();
// 29 Feb 2020 → 28 Feb 2021 (should be 0 days)
model._set("A1", "=DATEDIF(\"29/2/2020\", \"28/2/2021\", \"YD\")");
// 29 Feb 2020 → 1 Mar 2021 (should be 1 day)
model._set("A2", "=DATEDIF(\"29/2/2020\", \"2021-03-01\", \"YD\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"0");
assert_eq!(model._get_text("A2"), *"1");
}
#[test]
fn test_datedif_md_month_end_edge_cases() {
let mut model = new_empty_model();
// 31 Jan 2021 → 28 Feb 2021 (non-leap) => 28
model._set("B1", "=DATEDIF(\"31/1/2021\", \"28/2/2021\", \"MD\")");
// 31 Jan 2020 → 29 Feb 2020 (leap) => 29
model._set("B2", "=DATEDIF(\"31/1/2020\", \"29/2/2020\", \"MD\")");
model.evaluate();
assert_eq!(model._get_text("B1"), *"28");
assert_eq!(model._get_text("B2"), *"29");
}

View File

@@ -1,43 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_days360_month_end_us() {
let mut model = new_empty_model();
// 31 Jan 2021 -> 28 Feb 2021 (non-leap)
model._set("A1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28))");
// 31 Jan 2020 -> 28 Feb 2020 (leap year not last day of Feb)
model._set("A2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,28))");
// 28 Feb 2020 -> 31 Mar 2020 (leap year span crossing month ends)
model._set("A3", "=DAYS360(DATE(2020,2,28),DATE(2020,3,31))");
// 30 Apr 2021 -> 31 May 2021 (end-of-month adjustment rule)
model._set("A4", "=DAYS360(DATE(2021,4,30),DATE(2021,5,31))");
model.evaluate();
assert_eq!(model._get_text("A1"), *"30");
assert_eq!(model._get_text("A2"), *"28");
assert_eq!(model._get_text("A3"), *"33");
assert_eq!(model._get_text("A4"), *"30");
}
#[test]
fn test_days360_month_end_european() {
let mut model = new_empty_model();
// European basis = TRUE (or 1)
model._set("B1", "=DAYS360(DATE(2021,1,31),DATE(2021,2,28),TRUE)");
model._set("B2", "=DAYS360(DATE(2020,1,31),DATE(2020,2,29),TRUE)");
model._set("B3", "=DAYS360(DATE(2021,8,31),DATE(2021,9,30),TRUE)");
model.evaluate();
assert_eq!(model._get_text("B1"), *"28");
assert_eq!(model._get_text("B2"), *"29");
assert_eq!(model._get_text("B3"), *"30");
}

View File

@@ -1,22 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_degrees_radians_arguments() {
let mut model = new_empty_model();
model._set("A1", "=DEGREES()");
model._set("A2", "=RADIANS()");
model._set("A3", "=RADIANS(180)");
model._set("A4", "=RADIANS(180, 2)");
model._set("A5", "=DEGREES(RADIANS(180))");
model._set("A6", "=DEGREES(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"3.141592654");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"180");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -1,23 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=EVEN(2)");
model._set("A2", "=ODD(2)");
model._set("A3", "=EVEN()");
model._set("A4", "=ODD()");
model._set("A5", "=EVEN(1, 2)");
model._set("A6", "=ODD(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"3");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -1,26 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_arguments() {
let mut model = new_empty_model();
model._set("A1", "=EXP()");
model._set("A2", "=SIGN()");
model._set("A3", "=EXP(0)");
model._set("A4", "=SIGN(-10)");
model._set("A5", "=EXP(1, 2)");
model._set("A6", "=SIGN(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"1");
assert_eq!(model._get_text("A4"), *"-1");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -1,182 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::types::Cell;
// Helper to evaluate a formula and return the formatted text
fn eval_formula(formula: &str) -> String {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
model._get_text("A1")
}
// Helper that evaluates a formula and returns the raw value of A1 as a Result<f64, String>
fn eval_formula_raw_number(formula: &str) -> Result<f64, String> {
let mut model = new_empty_model();
model._set("A1", formula);
model.evaluate();
match model._get_cell("A1") {
Cell::NumberCell { v, .. } => Ok(*v),
Cell::BooleanCell { v, .. } => Ok(if *v { 1.0 } else { 0.0 }),
Cell::ErrorCell { ei, .. } => Err(format!("{}", ei)),
_ => Err(model._get_text("A1")),
}
}
#[test]
fn test_datevalue_basic_numeric() {
// DATEVALUE should return the serial number representing the date, **not** a formatted date
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2/1/2023\")").unwrap(),
44958.0
);
}
#[test]
fn test_datevalue_mmdd_with_leading_zero() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"02/01/2023\")").unwrap(),
44958.0
); // 1-Feb-2023
}
#[test]
fn test_datevalue_iso() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023-01-02\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_month_name() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2-Jan-23\")").unwrap(),
44928.0
);
}
#[test]
fn test_datevalue_ambiguous_ddmm() {
// 01/02/2023 interpreted as MM/DD -> 2-Jan-2023
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"01/02/2023\")").unwrap(),
44929.0
);
}
#[test]
fn test_datevalue_ddmm_unambiguous() {
// 15/01/2023 should be 15-Jan-2023 since 15 cannot be month
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"15/01/2023\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_leap_day() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"29/02/2020\")").unwrap(),
43890.0
);
}
#[test]
fn test_datevalue_year_first_text_month() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"2023/Jan/15\")").unwrap(),
44941.0
);
}
#[test]
fn test_datevalue_mmdd_with_day_gt_12() {
assert_eq!(
eval_formula_raw_number("=DATEVALUE(\"6/15/2021\")").unwrap(),
44373.0
);
}
#[test]
fn test_datevalue_error_conditions() {
let cases = [
"=DATEVALUE(\"31/04/2023\")", // invalid day (Apr has 30 days)
"=DATEVALUE(\"13/13/2023\")", // invalid month
"=DATEVALUE(\"not a date\")", // non-date text
];
for formula in cases {
let result = eval_formula(formula);
assert_eq!(result, *"#VALUE!", "Expected #VALUE! for {}", formula);
}
}
// Helper to set and evaluate a single DATEDIF call
fn eval_datedif(unit: &str) -> String {
let mut model = new_empty_model();
let formula = format!("=DATEDIF(\"2020-01-01\", \"2021-06-15\", \"{}\")", unit);
model._set("A1", &formula);
model.evaluate();
model._get_text("A1")
}
#[test]
fn test_datedif_y() {
assert_eq!(eval_datedif("Y"), *"1");
}
#[test]
fn test_datedif_m() {
assert_eq!(eval_datedif("M"), *"17");
}
#[test]
fn test_datedif_d() {
assert_eq!(eval_datedif("D"), *"531");
}
#[test]
fn test_datedif_ym() {
assert_eq!(eval_datedif("YM"), *"5");
}
#[test]
fn test_datedif_yd() {
assert_eq!(eval_datedif("YD"), *"165");
}
#[test]
fn test_datedif_md() {
assert_eq!(eval_datedif("MD"), *"14");
}
#[test]
fn test_datedif_edge_and_error_cases() {
let mut model = new_empty_model();
// Leap-year spanning
model._set("A1", "=DATEDIF(\"28/2/2020\", \"1/3/2020\", \"D\")");
// End date before start date => #NUM!
model._set("A2", "=DATEDIF(\"1/2/2021\", \"1/1/2021\", \"D\")");
// Invalid unit => #VALUE!
model._set("A3", "=DATEDIF(\"1/1/2020\", \"1/1/2021\", \"Z\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"2");
assert_eq!(model._get_text("A2"), *"#NUM!");
assert_eq!(model._get_text("A3"), *"#VALUE!");
}
#[test]
fn test_datedif_mixed_case_unit() {
assert_eq!(eval_datedif("yD"), *"165"); // mixed-case should work
}
#[test]
fn test_datedif_error_propagation() {
// Invalid date in arguments should propagate #VALUE!
let mut model = new_empty_model();
model._set("A1", "=DATEDIF(\"bad\", \"bad\", \"Y\")");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#VALUE!");
}

View File

@@ -1,24 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn datevalue_timevalue_arguments() {
let mut model = new_empty_model();
model._set("A1", "=DATEVALUE()");
model._set("A2", "=TIMEVALUE()");
model._set("A3", "=DATEVALUE("2000-01-01")")
model._set("A4", "=TIMEVALUE("12:00:00")")
model._set("A5", "=DATEVALUE(1,2)");
model._set("A6", "=TIMEVALUE(1,2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"36526");
assert_eq!(model._get_text("A4"), *"0.5");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -1,520 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
// Helper constants for common time values with detailed documentation
const MIDNIGHT: &str = "0"; // 00:00:00 = 0/24 = 0
const NOON: &str = "0.5"; // 12:00:00 = 12/24 = 0.5
const TIME_14_30: &str = "0.604166667"; // 14:30:00 = 14.5/24 ≈ 0.604166667
const TIME_14_30_45: &str = "0.6046875"; // 14:30:45 = 14.5125/24 = 0.6046875
const TIME_14_30_59: &str = "0.604849537"; // 14:30:59 (from floored fractional inputs)
const TIME_23_59_59: &str = "0.999988426"; // 23:59:59 = 23.99972.../24 ≈ 0.999988426
// Excel documentation test values with explanations
const TIME_2_24_AM: &str = "0.1"; // 2:24 AM = 2.4/24 = 0.1
const TIME_2_PM: &str = "0.583333333"; // 2:00 PM = 14/24 ≈ 0.583333333
const TIME_6_45_PM: &str = "0.78125"; // 6:45 PM = 18.75/24 = 0.78125
const TIME_6_35_AM: &str = "0.274305556"; // 6:35 AM = 6.583333.../24 ≈ 0.274305556
const TIME_2_30_AM: &str = "0.104166667"; // 2:30 AM = 2.5/24 ≈ 0.104166667
const TIME_1_AM: &str = "0.041666667"; // 1:00 AM = 1/24 ≈ 0.041666667
const TIME_9_PM: &str = "0.875"; // 9:00 PM = 21/24 = 0.875
const TIME_2_AM: &str = "0.083333333"; // 2:00 AM = 2/24 ≈ 0.083333333
// Additional helper: 1-second past midnight (00:00:01)
const TIME_00_00_01: &str = "0.000011574"; // 1 second = 1/86400 ≈ 0.000011574
/// Helper function to set up and evaluate a model with time expressions
fn test_time_expressions(expressions: &[(&str, &str)]) -> crate::model::Model {
let mut model = new_empty_model();
for (cell, formula) in expressions {
model._set(cell, formula);
}
model.evaluate();
model
}
/// Helper function to test component extraction for a given time value
/// Returns (hour, minute, second) as strings
fn test_component_extraction(time_value: &str) -> (String, String, String) {
let model = test_time_expressions(&[
("A1", &format!("=HOUR({time_value})")),
("B1", &format!("=MINUTE({time_value})")),
("C1", &format!("=SECOND({time_value})")),
]);
(
model._get_text("A1").to_string(),
model._get_text("B1").to_string(),
model._get_text("C1").to_string(),
)
}
#[test]
fn test_excel_timevalue_compatibility() {
// Test cases based on Excel's official documentation and examples
let model = test_time_expressions(&[
// Excel documentation examples
("A1", "=TIMEVALUE(\"2:24 AM\")"), // Should be 0.1
("A2", "=TIMEVALUE(\"2 PM\")"), // Should be 0.583333... (14/24)
("A3", "=TIMEVALUE(\"6:45 PM\")"), // Should be 0.78125 (18.75/24)
("A4", "=TIMEVALUE(\"18:45\")"), // Same as above, 24-hour format
// Date-time format (date should be ignored)
("B1", "=TIMEVALUE(\"22-Aug-2011 6:35 AM\")"), // Should be ~0.2743
("B2", "=TIMEVALUE(\"2023-01-01 14:30:00\")"), // Should be 0.604166667
// Edge cases that Excel should support
("C1", "=TIMEVALUE(\"12:00 AM\")"), // Midnight: 0
("C2", "=TIMEVALUE(\"12:00 PM\")"), // Noon: 0.5
("C3", "=TIMEVALUE(\"11:59:59 PM\")"), // Almost midnight: 0.999988426
// Single digit variations
("D1", "=TIMEVALUE(\"1 AM\")"), // 1:00 AM
("D2", "=TIMEVALUE(\"9 PM\")"), // 9:00 PM
("D3", "=TIMEVALUE(\"12 AM\")"), // Midnight
("D4", "=TIMEVALUE(\"12 PM\")"), // Noon
]);
// Excel documentation examples - verify exact values
assert_eq!(model._get_text("A1"), *TIME_2_24_AM); // 2:24 AM
assert_eq!(model._get_text("A2"), *TIME_2_PM); // 2 PM = 14:00
assert_eq!(model._get_text("A3"), *TIME_6_45_PM); // 6:45 PM = 18:45
assert_eq!(model._get_text("A4"), *TIME_6_45_PM); // 18:45 (24-hour)
// Date-time formats (date ignored, extract time only)
assert_eq!(model._get_text("B1"), *TIME_6_35_AM); // 6:35 AM ≈ 0.2743
assert_eq!(model._get_text("B2"), *TIME_14_30); // 14:30:00
// Edge cases
assert_eq!(model._get_text("C1"), *MIDNIGHT); // 12:00 AM = 00:00
assert_eq!(model._get_text("C2"), *NOON); // 12:00 PM = 12:00
assert_eq!(model._get_text("C3"), *TIME_23_59_59); // 11:59:59 PM
// Single digit hours
assert_eq!(model._get_text("D1"), *TIME_1_AM); // 1:00 AM
assert_eq!(model._get_text("D2"), *TIME_9_PM); // 9:00 PM = 21:00
assert_eq!(model._get_text("D3"), *MIDNIGHT); // 12 AM = 00:00
assert_eq!(model._get_text("D4"), *NOON); // 12 PM = 12:00
}
#[test]
fn test_time_function_basic_cases() {
let model = test_time_expressions(&[
("A1", "=TIME(0,0,0)"), // Midnight
("A2", "=TIME(12,0,0)"), // Noon
("A3", "=TIME(14,30,0)"), // 2:30 PM
("A4", "=TIME(23,59,59)"), // Max time
]);
assert_eq!(model._get_text("A1"), *MIDNIGHT);
assert_eq!(model._get_text("A2"), *NOON);
assert_eq!(model._get_text("A3"), *TIME_14_30);
assert_eq!(model._get_text("A4"), *TIME_23_59_59);
}
#[test]
fn test_time_function_normalization() {
let model = test_time_expressions(&[
("A1", "=TIME(25,0,0)"), // Hours > 24 wrap around
("A2", "=TIME(48,0,0)"), // 48 hours = 0 (2 full days)
("A3", "=TIME(0,90,0)"), // 90 minutes = 1.5 hours
("A4", "=TIME(0,0,90)"), // 90 seconds = 1.5 minutes
("A5", "=TIME(14.9,30.9,59.9)"), // Fractional inputs floored to 14:30:59
]);
assert_eq!(model._get_text("A1"), *TIME_1_AM); // 1:00:00
assert_eq!(model._get_text("A2"), *MIDNIGHT); // 0:00:00
assert_eq!(model._get_text("A3"), *"0.0625"); // 1:30:00
assert_eq!(model._get_text("A4"), *"0.001041667"); // 0:01:30
assert_eq!(model._get_text("A5"), *TIME_14_30_59); // 14:30:59 (floored)
}
#[test]
fn test_time_function_precision_edge_cases() {
let model = test_time_expressions(&[
// High precision fractional seconds
("A1", "=TIME(14,30,45.999)"), // Fractional seconds should be floored
("A2", "=SECOND(TIME(14,30,45.999))"), // Should extract 45, not 46
// Very large normalization values
("B1", "=TIME(999,999,999)"), // Extreme normalization test
("B2", "=HOUR(999.5)"), // Multiple days, extract hour from fractional part
("B3", "=MINUTE(999.75)"), // Multiple days, extract minute
// Boundary conditions at rollover points
("C1", "=TIME(24,60,60)"), // Should normalize to next day (00:01:00)
("C2", "=HOUR(0.999999999)"), // Almost 24 hours should be 23
("C3", "=MINUTE(0.999999999)"), // Almost 24 hours, extract minutes
("C4", "=SECOND(0.999999999)"), // Almost 24 hours, extract seconds
// Precision at boundaries
("D1", "=TIME(23,59,59.999)"), // Very close to midnight
("D2", "=TIME(0,0,0.001)"), // Just after midnight
]);
// Fractional seconds are floored
assert_eq!(model._get_text("A2"), *"45"); // 45.999 floored to 45
// Multiple days should work with rem_euclid
assert_eq!(model._get_text("B2"), *"12"); // 999.5 days, hour = 12 (noon)
// Boundary normalization
assert_eq!(model._get_text("C1"), *"0.042361111"); // 24:60:60 = 01:01:00 (normalized)
assert_eq!(model._get_text("C2"), *"23"); // Almost 24 hours = 23:xx:xx
// High precision should be handled correctly
let result_d1 = model._get_text("D1").parse::<f64>().unwrap();
assert!(result_d1 < 1.0 && result_d1 > 0.999); // Very close to but less than 1.0
}
#[test]
fn test_time_function_errors() {
let model = test_time_expressions(&[
("A1", "=TIME()"), // Wrong arg count
("A2", "=TIME(12)"), // Wrong arg count
("A3", "=TIME(12,30,0,0)"), // Wrong arg count
("B1", "=TIME(-1,0,0)"), // Negative hour
("B2", "=TIME(0,-1,0)"), // Negative minute
("B3", "=TIME(0,0,-1)"), // Negative second
]);
// Wrong argument count
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
// Negative values should return #NUM! error
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
}
#[test]
fn test_timevalue_function_formats() {
let model = test_time_expressions(&[
// Basic formats
("A1", "=TIMEVALUE(\"14:30\")"),
("A2", "=TIMEVALUE(\"14:30:45\")"),
("A3", "=TIMEVALUE(\"00:00:00\")"),
// AM/PM formats
("B1", "=TIMEVALUE(\"2:30 PM\")"),
("B2", "=TIMEVALUE(\"2:30 AM\")"),
("B3", "=TIMEVALUE(\"12:00 PM\")"), // Noon
("B4", "=TIMEVALUE(\"12:00 AM\")"), // Midnight
// Single hour with AM/PM (now supported!)
("B5", "=TIMEVALUE(\"2 PM\")"),
("B6", "=TIMEVALUE(\"2 AM\")"),
// Date-time formats (extract time only)
("C1", "=TIMEVALUE(\"2023-01-01 14:30:00\")"),
("C2", "=TIMEVALUE(\"2023-01-01T14:30:00\")"),
// Whitespace handling
("D1", "=TIMEVALUE(\" 14:30 \")"),
]);
// Basic formats
assert_eq!(model._get_text("A1"), *TIME_14_30);
assert_eq!(model._get_text("A2"), *TIME_14_30_45);
assert_eq!(model._get_text("A3"), *MIDNIGHT);
// AM/PM formats
assert_eq!(model._get_text("B1"), *TIME_14_30); // 2:30 PM = 14:30
assert_eq!(model._get_text("B2"), *TIME_2_30_AM); // 2:30 AM
assert_eq!(model._get_text("B3"), *NOON); // 12:00 PM = noon
assert_eq!(model._get_text("B4"), *MIDNIGHT); // 12:00 AM = midnight
// Single hour AM/PM formats (now supported!)
assert_eq!(model._get_text("B5"), *TIME_2_PM); // 2 PM = 14:00
assert_eq!(model._get_text("B6"), *TIME_2_AM); // 2 AM = 02:00
// Date-time formats
assert_eq!(model._get_text("C1"), *TIME_14_30);
assert_eq!(model._get_text("C2"), *TIME_14_30);
// Whitespace
assert_eq!(model._get_text("D1"), *TIME_14_30);
}
#[test]
fn test_timevalue_function_errors() {
let model = test_time_expressions(&[
("A1", "=TIMEVALUE()"), // Wrong arg count
("A2", "=TIMEVALUE(\"14:30\", \"x\")"), // Wrong arg count
("B1", "=TIMEVALUE(\"invalid\")"), // Invalid format
("B2", "=TIMEVALUE(\"25:00\")"), // Invalid hour
("B3", "=TIMEVALUE(\"14:70\")"), // Invalid minute
("B4", "=TIMEVALUE(\"\")"), // Empty string
("B5", "=TIMEVALUE(\"2PM\")"), // Missing space (still unsupported)
]);
// Wrong argument count should return #ERROR!
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
// Invalid formats should return #VALUE!
assert_eq!(model._get_text("B1"), *"#VALUE!");
assert_eq!(model._get_text("B2"), *"#VALUE!");
assert_eq!(model._get_text("B3"), *"#VALUE!");
assert_eq!(model._get_text("B4"), *"#VALUE!");
assert_eq!(model._get_text("B5"), *"#VALUE!"); // "2PM" no space - not supported
}
#[test]
fn test_time_component_extraction_comprehensive() {
// Test component extraction using helper function for consistency
// Test basic time values
let test_cases = [
(MIDNIGHT, ("0", "0", "0")), // 00:00:00
(NOON, ("12", "0", "0")), // 12:00:00
(TIME_14_30, ("14", "30", "0")), // 14:30:00
(TIME_23_59_59, ("23", "59", "59")), // 23:59:59
];
for (time_value, expected) in test_cases {
let (hour, minute, second) = test_component_extraction(time_value);
assert_eq!(hour, expected.0, "Hour mismatch for {time_value}");
assert_eq!(minute, expected.1, "Minute mismatch for {time_value}");
assert_eq!(second, expected.2, "Second mismatch for {time_value}");
}
// Test multiple days (extract from fractional part)
let (hour, minute, second) = test_component_extraction("1.5"); // Day 2, 12:00
assert_eq!(
(hour, minute, second),
("12".to_string(), "0".to_string(), "0".to_string())
);
let (hour, minute, second) = test_component_extraction("100.604166667"); // Day 101, 14:30
assert_eq!(
(hour, minute, second),
("14".to_string(), "30".to_string(), "0".to_string())
);
// Test precision at boundaries
let (hour, _, _) = test_component_extraction("0.041666666"); // Just under 1:00 AM
assert_eq!(hour, "0");
let (hour, _, _) = test_component_extraction("0.041666667"); // Exactly 1:00 AM
assert_eq!(hour, "1");
let (hour, _, _) = test_component_extraction("0.041666668"); // Just over 1:00 AM
assert_eq!(hour, "1");
// Test very large day values
let (hour, minute, second) = test_component_extraction("1000000.25"); // Million days + 6 hours
assert_eq!(
(hour, minute, second),
("6".to_string(), "0".to_string(), "0".to_string())
);
}
#[test]
fn test_time_component_function_errors() {
let model = test_time_expressions(&[
// Wrong argument counts
("A1", "=HOUR()"), // No arguments
("A2", "=MINUTE()"), // No arguments
("A3", "=SECOND()"), // No arguments
("A4", "=HOUR(1, 2)"), // Too many arguments
("A5", "=MINUTE(1, 2)"), // Too many arguments
("A6", "=SECOND(1, 2)"), // Too many arguments
// Negative values should return #NUM!
("B1", "=HOUR(-0.5)"), // Negative value
("B2", "=MINUTE(-1)"), // Negative value
("B3", "=SECOND(-1)"), // Negative value
("B4", "=HOUR(-0.000001)"), // Slightly negative
("B5", "=MINUTE(-0.000001)"), // Slightly negative
("B6", "=SECOND(-0.000001)"), // Slightly negative
]);
// Wrong argument count should return #ERROR!
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
// Negative values should return #NUM!
assert_eq!(model._get_text("B1"), *"#NUM!");
assert_eq!(model._get_text("B2"), *"#NUM!");
assert_eq!(model._get_text("B3"), *"#NUM!");
assert_eq!(model._get_text("B4"), *"#NUM!");
assert_eq!(model._get_text("B5"), *"#NUM!");
assert_eq!(model._get_text("B6"), *"#NUM!");
}
#[test]
fn test_time_functions_integration() {
// Test how TIME, TIMEVALUE and component extraction functions work together
let model = test_time_expressions(&[
// Create times with both functions
("A1", "=TIME(14,30,45)"),
("A2", "=TIMEVALUE(\"14:30:45\")"),
// Extract components from TIME function results
("B1", "=HOUR(A1)"),
("B2", "=MINUTE(A1)"),
("B3", "=SECOND(A1)"),
// Extract components from TIMEVALUE function results
("C1", "=HOUR(A2)"),
("C2", "=MINUTE(A2)"),
("C3", "=SECOND(A2)"),
// Test additional TIME variations
("D1", "=TIME(14,0,0)"), // 14:00:00
("E1", "=HOUR(D1)"), // Extract hour from 14:00:00
("E2", "=MINUTE(D1)"), // Extract minute from 14:00:00
("E3", "=SECOND(D1)"), // Extract second from 14:00:00
]);
// TIME and TIMEVALUE should produce equivalent results
assert_eq!(model._get_text("A1"), model._get_text("A2"));
// Extracting components should work consistently
assert_eq!(model._get_text("B1"), *"14");
assert_eq!(model._get_text("B2"), *"30");
assert_eq!(model._get_text("B3"), *"45");
assert_eq!(model._get_text("C1"), *"14");
assert_eq!(model._get_text("C2"), *"30");
assert_eq!(model._get_text("C3"), *"45");
// Components from TIME(14,0,0)
assert_eq!(model._get_text("E1"), *"14");
assert_eq!(model._get_text("E2"), *"0");
assert_eq!(model._get_text("E3"), *"0");
}
#[test]
fn test_time_function_extreme_values() {
// Test missing edge cases: very large fractional inputs
let model = test_time_expressions(&[
// Extremely large fractional values to TIME function
("A1", "=TIME(999999.9, 999999.9, 999999.9)"), // Very large fractional inputs
("A2", "=TIME(1e6, 1e6, 1e6)"), // Scientific notation inputs
("A3", "=TIME(0.000001, 0.000001, 0.000001)"), // Very small fractional inputs
// Large day values for component extraction (stress test)
("B1", "=HOUR(999999.999)"), // Almost a million days
("B2", "=MINUTE(999999.999)"),
("B3", "=SECOND(999999.999)"),
// Edge case: exactly 1.0 (should be midnight of next day)
("C1", "=HOUR(1.0)"),
("C2", "=MINUTE(1.0)"),
("C3", "=SECOND(1.0)"),
// Very high precision values
("D1", "=HOUR(0.999999999999)"), // Almost exactly 24:00:00
("D2", "=MINUTE(0.999999999999)"),
("D3", "=SECOND(0.999999999999)"),
]);
// Large fractional inputs should be floored and normalized
let result_a1 = model._get_text("A1").parse::<f64>().unwrap();
assert!(
(0.0..1.0).contains(&result_a1),
"Result should be valid time fraction"
);
// Component extraction should work with very large values
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_b1), "Hour should be 0-23");
// Exactly 1.0 should be midnight (start of next day)
assert_eq!(model._get_text("C1"), *"0");
assert_eq!(model._get_text("C2"), *"0");
assert_eq!(model._get_text("C3"), *"0");
// Very high precision should still extract valid components
let hour_d1 = model._get_text("D1").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_d1), "Hour should be 0-23");
}
#[test]
fn test_timevalue_malformed_but_parseable() {
// Test missing edge case: malformed but potentially parseable strings
let model = test_time_expressions(&[
// Test various malformed but potentially parseable time strings
("A1", "=TIMEVALUE(\"14:30:00.123\")"), // Milliseconds (might be truncated)
("A2", "=TIMEVALUE(\"14:30:00.999\")"), // High precision milliseconds
("A3", "=TIMEVALUE(\"02:30:00\")"), // Leading zero hours
("A4", "=TIMEVALUE(\"2:05:00\")"), // Single digit hour, zero-padded minute
// Boundary cases for AM/PM parsing
("B1", "=TIMEVALUE(\"11:59:59 PM\")"), // Just before midnight
("B2", "=TIMEVALUE(\"12:00:01 AM\")"), // Just after midnight
("B3", "=TIMEVALUE(\"12:00:01 PM\")"), // Just after noon
("B4", "=TIMEVALUE(\"11:59:59 AM\")"), // Just before noon
// Test various date-time combinations
("C1", "=TIMEVALUE(\"2023-12-31T23:59:59\")"), // ISO format at year end
("C2", "=TIMEVALUE(\"2023-01-01 00:00:01\")"), // New year, just after midnight
// Test potential edge cases that might still be parseable
("D1", "=TIMEVALUE(\"24:00:00\")"), // Should error (invalid hour)
("D2", "=TIMEVALUE(\"23:60:00\")"), // Should error (invalid minute)
("D3", "=TIMEVALUE(\"23:59:60\")"), // Should error (invalid second)
]);
// Milliseconds are not supported, should return a #VALUE! error like Excel
assert_eq!(model._get_text("A1"), *"#VALUE!");
assert_eq!(model._get_text("A2"), *"#VALUE!");
// Leading zeros should work fine
assert_eq!(model._get_text("A3"), *TIME_2_30_AM); // 02:30:00 should parse as 2:30:00
// AM/PM boundary cases should work
let result_b1 = model._get_text("B1").parse::<f64>().unwrap();
assert!(
result_b1 > 0.99 && result_b1 < 1.0,
"11:59:59 PM should be very close to 1.0"
);
let result_b2 = model._get_text("B2").parse::<f64>().unwrap();
assert!(
result_b2 > 0.0 && result_b2 < 0.01,
"12:00:01 AM should be very close to 0.0"
);
// ISO 8601 format with "T" separator should be parsed correctly
assert_eq!(model._get_text("C1"), *TIME_23_59_59); // 23:59:59 → almost midnight
assert_eq!(model._get_text("C2"), *TIME_00_00_01); // 00:00:01 → one second past midnight
// Time parser normalizes edge cases to midnight (Excel compatibility)
assert_eq!(model._get_text("D1"), *"0"); // 24:00:00 = midnight of next day
assert_eq!(model._get_text("D2"), *"0"); // 23:60:00 normalizes to 24:00:00 = midnight
assert_eq!(model._get_text("D3"), *"0"); // 23:59:60 normalizes to 24:00:00 = midnight
}
#[test]
fn test_performance_stress_with_extreme_values() {
// Test performance/stress cases with extreme values
let model = test_time_expressions(&[
// Very large numbers that should still work
("A1", "=TIME(2147483647, 0, 0)"), // Max i32 hours
("A2", "=TIME(0, 2147483647, 0)"), // Max i32 minutes
("A3", "=TIME(0, 0, 2147483647)"), // Max i32 seconds
// Component extraction with extreme day values
("B1", "=HOUR(1e15)"), // Very large day number
("B2", "=MINUTE(1e15)"),
("B3", "=SECOND(1e15)"),
// Edge of floating point precision
("C1", "=HOUR(1.7976931348623157e+308)"), // Near max f64
("C2", "=HOUR(2.2250738585072014e-308)"), // Near min positive f64
// Multiple TIME function calls with large values
("D1", "=TIME(1000000, 1000000, 1000000)"), // Large normalized values
("D2", "=HOUR(D1)"), // Extract from large TIME result
("D3", "=MINUTE(D1)"),
("D4", "=SECOND(D1)"),
]);
// All results should be valid (not errors) even with extreme inputs
for cell in ["A1", "A2", "A3", "B1", "B2", "B3", "D1", "D2", "D3", "D4"] {
let result = model._get_text(cell);
assert!(
result != *"#ERROR!" && result != *"#NUM!" && result != *"#VALUE!",
"Cell {cell} should not error with extreme values: {result}",
);
}
// Results should be mathematically valid
let hour_b1 = model._get_text("B1").parse::<i32>().unwrap();
let minute_b2 = model._get_text("B2").parse::<i32>().unwrap();
let second_b3 = model._get_text("B3").parse::<i32>().unwrap();
assert!((0..=23).contains(&hour_b1));
assert!((0..=59).contains(&minute_b2));
assert!((0..=59).contains(&second_b3));
// TIME function results should be valid time fractions
let time_d1 = model._get_text("D1").parse::<f64>().unwrap();
assert!(
(0.0..1.0).contains(&time_d1),
"TIME result should be valid fraction"
);
}

View File

@@ -1,13 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn issue_155() {
let mut model = new_empty_model();
model._set("A1", "123");
model._set("D2", "=-(A1^1.22)");
model.evaluate();
assert_eq!(model._get_formula("D2"), "=-(A1^1.22)".to_string());
}

View File

@@ -1,22 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=MOD(5,2)");
model._set("A2", "=MOD()");
model._set("A3", "=MOD(5, 2, 1)");
model._set("A4", "=QUOTIENT(5, 2)");
model._set("A5", "=QUOTIENT()");
model._set("A6", "=QUOTIENT(5, 2, 1)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"1");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"2");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
}

View File

@@ -1,40 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=MROUND()");
model._set("A2", "=MROUND(10)");
model._set("A3", "=MROUND(10, 3)");
model._set("A4", "=MROUND(10, 3, 1)");
model._set("A5", "=TRUNC()");
model._set("A6", "=TRUNC(10)");
model._set("A7", "=TRUNC(10.22, 1)");
model._set("A8", "=TRUNC(10, 3, 1)");
model._set("A9", "=INT()");
model._set("A10", "=INT(10.22)");
model._set("A11", "=INT(10.22, 1)");
model._set("A12", "=INT(10.22, 1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"9");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"10.2");
assert_eq!(model._get_text("A8"), *"#ERROR!");
assert_eq!(model._get_text("A9"), *"#ERROR!");
assert_eq!(model._get_text("A10"), *"10");
assert_eq!(model._get_text("A11"), *"#ERROR!");
assert_eq!(model._get_text("A12"), *"#ERROR!");
}

View File

@@ -1,347 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
// Test data: Jan 1-10, 2023 week
const JAN_1_2023: i32 = 44927; // Sunday
const JAN_2_2023: i32 = 44928; // Monday
const JAN_6_2023: i32 = 44932; // Friday
const JAN_9_2023: i32 = 44935; // Monday
const JAN_10_2023: i32 = 44936; // Tuesday
#[test]
fn networkdays_calculates_weekdays_excluding_weekends() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Should count 7 weekdays in 10-day span"
);
}
#[test]
fn networkdays_handles_reverse_date_order() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_10_2023},{JAN_1_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"-7",
"Reversed dates should return negative count"
);
}
#[test]
fn networkdays_excludes_holidays_from_weekdays() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{JAN_9_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"6",
"Should exclude Monday holiday from 7 weekdays"
);
}
#[test]
fn networkdays_handles_same_start_end_date() {
let mut model = new_empty_model();
model._set("A1", &format!("=NETWORKDAYS({JAN_9_2023},{JAN_9_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"1",
"Same weekday date should count as 1 workday"
);
}
#[test]
fn networkdays_accepts_holiday_ranges() {
let mut model = new_empty_model();
model._set("B1", &JAN_2_2023.to_string());
model._set("B2", &JAN_6_2023.to_string());
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B2)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"5",
"Should exclude 2 holidays from 7 weekdays"
);
}
#[test]
fn networkdays_intl_uses_standard_weekend_by_default() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Default should be Saturday-Sunday weekend"
);
}
#[test]
fn networkdays_intl_supports_numeric_weekend_patterns() {
let mut model = new_empty_model();
// Pattern 2 = Sunday-Monday weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},2)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"6",
"Sunday-Monday weekend should give 6 workdays"
);
}
#[test]
fn networkdays_intl_supports_single_day_weekends() {
let mut model = new_empty_model();
// Pattern 11 = Sunday only weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},11)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"8",
"Sunday-only weekend should give 8 workdays"
);
}
#[test]
fn networkdays_intl_supports_string_weekend_patterns() {
let mut model = new_empty_model();
// "0000110" = Friday-Saturday weekend
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\")"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"8",
"Friday-Saturday weekend should give 8 workdays"
);
}
#[test]
fn networkdays_intl_no_weekends_counts_all_days() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000000\")"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"10",
"No weekends should count all 10 days"
);
}
#[test]
fn networkdays_intl_combines_custom_weekends_with_holidays() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS.INTL({JAN_1_2023},{JAN_10_2023},\"0000110\",{JAN_9_2023})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Should exclude both weekend and holiday"
);
}
#[test]
fn networkdays_validates_argument_count() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS()");
model._set("A2", "=NETWORKDAYS(1,2,3,4)");
model._set("A3", "=NETWORKDAYS.INTL()");
model._set("A4", "=NETWORKDAYS.INTL(1,2,3,4,5)");
model.evaluate();
assert_eq!(model._get_text("A1"), "#ERROR!");
assert_eq!(model._get_text("A2"), "#ERROR!");
assert_eq!(model._get_text("A3"), "#ERROR!");
assert_eq!(model._get_text("A4"), "#ERROR!");
}
#[test]
fn networkdays_rejects_invalid_dates() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(-1,100)");
model._set("A2", "=NETWORKDAYS(1,3000000)");
model._set("A3", "=NETWORKDAYS(\"text\",100)");
model.evaluate();
assert_eq!(model._get_text("A1"), "#NUM!");
assert_eq!(model._get_text("A2"), "#NUM!");
assert_eq!(model._get_text("A3"), "#VALUE!");
}
#[test]
fn networkdays_intl_rejects_invalid_weekend_patterns() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS.INTL(1,10,99)");
model._set("A2", "=NETWORKDAYS.INTL(1,10,\"111110\")");
model._set("A3", "=NETWORKDAYS.INTL(1,10,\"11111000\")");
model._set("A4", "=NETWORKDAYS.INTL(1,10,\"1111102\")");
model.evaluate();
assert_eq!(model._get_text("A1"), "#NUM!");
assert_eq!(model._get_text("A2"), "#VALUE!");
assert_eq!(model._get_text("A3"), "#VALUE!");
assert_eq!(model._get_text("A4"), "#VALUE!");
}
#[test]
fn networkdays_rejects_invalid_holidays() {
let mut model = new_empty_model();
model._set("B1", "invalid");
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1)"),
);
model._set(
"A2",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},-1)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"#VALUE!",
"Should reject non-numeric holidays"
);
assert_eq!(
model._get_text("A2"),
"#NUM!",
"Should reject out-of-range holidays"
);
}
#[test]
fn networkdays_handles_weekend_only_periods() {
let mut model = new_empty_model();
let saturday = JAN_1_2023 - 1;
model._set("A1", &format!("=NETWORKDAYS({saturday},{JAN_1_2023})"));
model.evaluate();
assert_eq!(
model._get_text("A1"),
"0",
"Weekend-only period should count 0 workdays"
);
}
#[test]
fn networkdays_ignores_holidays_outside_date_range() {
let mut model = new_empty_model();
let future_holiday = JAN_10_2023 + 100;
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},{future_holiday})"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Out-of-range holidays should be ignored"
);
}
#[test]
fn networkdays_handles_empty_holiday_ranges() {
let mut model = new_empty_model();
model._set(
"A1",
&format!("=NETWORKDAYS({JAN_1_2023},{JAN_10_2023},B1:B3)"),
);
model.evaluate();
assert_eq!(
model._get_text("A1"),
"7",
"Empty holiday range should be treated as no holidays"
);
}
#[test]
fn networkdays_handles_minimum_valid_dates() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(1,7)");
model.evaluate();
assert_eq!(
model._get_text("A1"),
"5",
"Should handle earliest Excel dates correctly"
);
}
#[test]
fn networkdays_handles_large_date_ranges_efficiently() {
let mut model = new_empty_model();
model._set("A1", "=NETWORKDAYS(1,365)");
model.evaluate();
assert!(
!model._get_text("A1").starts_with('#'),
"Large ranges should not error"
);
}

View File

@@ -1,30 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::{mock_time, test::util::new_empty_model};
// 14:44 20 Mar 2023 Berlin
const TIMESTAMP_2023: i64 = 1679319865208;
#[test]
fn arguments() {
let mut model = new_empty_model();
model._set("A1", "=NOW(1)");
model.evaluate();
assert_eq!(
model._get_text("A1"),
"#ERROR!",
"NOW should not accept arguments"
);
}
#[test]
fn returns_date_time() {
mock_time::set_mock_time(TIMESTAMP_2023);
let mut model = new_empty_model();
model._set("A1", "=NOW()");
model.evaluate();
let text = model._get_text("A1");
assert_eq!(text, *"20/03/2023 13:44:25");
}

View File

@@ -8,15 +8,6 @@ fn test_simple_format() {
assert_eq!(formatted.text, "2.3".to_string()); assert_eq!(formatted.text, "2.3".to_string());
} }
#[test]
fn test_maximum_zeros() {
let formatted = format_number(1.0 / 3.0, "#,##0.0000000000000000000", "en");
assert_eq!(formatted.text, "0.3333333333333330000".to_string());
let formatted = format_number(1234.0 + 1.0 / 3.0, "#,##0.0000000000000000000", "en");
assert_eq!(formatted.text, "1,234.3333333333300000000".to_string());
}
#[test] #[test]
#[ignore = "not yet implemented"] #[ignore = "not yet implemented"]
fn test_wrong_locale() { fn test_wrong_locale() {

View File

@@ -1,16 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn sheet_number_name() {
let mut model = new_empty_model();
model.new_sheet();
model._set("A1", "7");
model._set("A2", "=Sheet2!C3");
model.evaluate();
model.rename_sheet("Sheet2", "2024").unwrap();
model.evaluate();
assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1", "2024"]);
assert_eq!(model._get_text("A2"), "0");
}

View File

@@ -33,8 +33,7 @@ fn now_basic_utc() {
model.evaluate(); model.evaluate();
assert_eq!(model._get_text("A1"), *"20/03/2023"); assert_eq!(model._get_text("A1"), *"20/03/2023");
// 45005.572511574 assert_eq!(model._get_text("A2"), *"45005.572511574");
assert_eq!(model._get_text("A2"), *"20/03/2023 13:44:25");
} }
#[test] #[test]
@@ -47,5 +46,5 @@ fn now_basic_europe_berlin() {
assert_eq!(model._get_text("A1"), *"20/03/2023"); assert_eq!(model._get_text("A1"), *"20/03/2023");
// This is UTC + 1 hour: 45005.572511574 + 1/24 // This is UTC + 1 hour: 45005.572511574 + 1/24
assert_eq!(model._get_text("A2"), *"20/03/2023 14:44:25"); assert_eq!(model._get_text("A2"), *"45005.614178241");
} }

View File

@@ -96,14 +96,3 @@ fn test_fn_tan_pi2() {
// This is consistent with IEEE 754 but inconsistent with Excel // This is consistent with IEEE 754 but inconsistent with Excel
assert_eq!(model._get_text("A1"), *"1.63312E+16"); assert_eq!(model._get_text("A1"), *"1.63312E+16");
} }
#[test]
fn test_trigonometric_identity() {
let mut model = new_empty_model();
model._set("A1", "=COTH(1)*CSCH(1)");
model._set("A2", "=COSH(1)/(SINH(1))^2");
model._set("A3", "=A1=A2");
model.evaluate();
assert_eq!(model._get_text("A3"), *"TRUE");
}

View File

@@ -1,53 +0,0 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
#[test]
fn fn_arguments() {
let mut model = new_empty_model();
model._set("A1", "=CSC()");
model._set("A2", "=SEC()");
model._set("A3", "=COT()");
model._set("A4", "=CSCH()");
model._set("A5", "=SECH()");
model._set("A6", "=COTH()");
model._set("A7", "=ACOT()");
model._set("A8", "=ACOTH()");
model._set("B1", "=CSC(1, 2)");
model._set("B2", "=SEC(1, 2)");
model._set("B3", "=COT(1, 2)");
model._set("B4", "=CSCH(1, 2)");
model._set("B5", "=SECH(1, 2)");
model._set("B6", "=COTH(1, 2)");
model._set("B7", "=ACOT(1, 2)");
model._set("B8", "=ACOTH(1, 2)");
model.evaluate();
assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
assert_eq!(model._get_text("A3"), *"#ERROR!");
assert_eq!(model._get_text("A4"), *"#ERROR!");
assert_eq!(model._get_text("A5"), *"#ERROR!");
assert_eq!(model._get_text("A6"), *"#ERROR!");
assert_eq!(model._get_text("A7"), *"#ERROR!");
assert_eq!(model._get_text("A8"), *"#ERROR!");
assert_eq!(model._get_text("B1"), *"#ERROR!");
assert_eq!(model._get_text("B2"), *"#ERROR!");
assert_eq!(model._get_text("B3"), *"#ERROR!");
assert_eq!(model._get_text("B4"), *"#ERROR!");
assert_eq!(model._get_text("B5"), *"#ERROR!");
assert_eq!(model._get_text("B6"), *"#ERROR!");
assert_eq!(model._get_text("B7"), *"#ERROR!");
assert_eq!(model._get_text("B8"), *"#ERROR!");
}

View File

@@ -1,26 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_weekday_return_types_11_to_17() {
let mut model = new_empty_model();
// Test date: 44561 corresponds to a Friday (2021-12-31). We verify the
// numeric result for each custom week start defined by return_type 11-17.
model._set("A1", "=WEEKDAY(44561,11)"); // Monday start
model._set("A2", "=WEEKDAY(44561,12)"); // Tuesday start
model._set("A3", "=WEEKDAY(44561,13)"); // Wednesday start
model._set("A4", "=WEEKDAY(44561,14)"); // Thursday start
model._set("A5", "=WEEKDAY(44561,15)"); // Friday start
model._set("A6", "=WEEKDAY(44561,16)"); // Saturday start
model._set("A7", "=WEEKDAY(44561,17)"); // Sunday start
model.evaluate();
assert_eq!(model._get_text("A1"), *"5"); // Mon=1 .. Sun=7 ⇒ Fri=5
assert_eq!(model._get_text("A2"), *"4"); // Tue start ⇒ Fri=4
assert_eq!(model._get_text("A3"), *"3"); // Wed start ⇒ Fri=3
assert_eq!(model._get_text("A4"), *"2"); // Thu start ⇒ Fri=2
assert_eq!(model._get_text("A5"), *"1"); // Fri start ⇒ Fri=1
assert_eq!(model._get_text("A6"), *"7"); // Sat start ⇒ Fri=7
assert_eq!(model._get_text("A7"), *"6"); // Sun start ⇒ Fri=6
}

View File

@@ -1,31 +0,0 @@
use crate::test::util::new_empty_model;
#[test]
fn test_weeknum_return_types_11_to_17_and_21() {
let mut model = new_empty_model();
// Date 44561 -> 2021-12-31 (Friday). Previously verified as week 53 (Sunday/Monday start).
// We verify that custom week-start codes 11-17 all map to week 53 and ISO variant (21) maps to 52.
let formulas = [
("A1", "=WEEKNUM(44561,11)"),
("A2", "=WEEKNUM(44561,12)"),
("A3", "=WEEKNUM(44561,13)"),
("A4", "=WEEKNUM(44561,14)"),
("A5", "=WEEKNUM(44561,15)"),
("A6", "=WEEKNUM(44561,16)"),
("A7", "=WEEKNUM(44561,17)"),
("A8", "=WEEKNUM(44561,21)"), // ISO week numbering
];
for (cell, formula) in formulas {
model._set(cell, formula);
}
model.evaluate();
// All 11-17 variations should yield 53
for cell in ["A1", "A2", "A3", "A4", "A5", "A6", "A7"] {
assert_eq!(model._get_text(cell), *"53", "{cell} should be 53");
}
// ISO week (return_type 21)
assert_eq!(model._get_text("A8"), *"52");
}

View File

@@ -1,60 +0,0 @@
#![allow(clippy::panic)]
use crate::{cell::CellValue, test::util::new_empty_model};
#[test]
fn test_yearfrac_basis_2_actual_360() {
let mut model = new_empty_model();
// Non-leap span of exactly 360 days should result in 1.0
model._set("A1", "=YEARFRAC(44561,44921,2)");
// Leap-year span of 366 days: Jan 1 2020 → Jan 1 2021
model._set("A2", "=YEARFRAC(43831,44197,2)");
// Reverse order should yield negative value
model._set("A3", "=YEARFRAC(44921,44561,2)");
model.evaluate();
// 360/360
assert_eq!(model._get_text("A1"), *"1");
// 366/360 ≈ 1.0166666667 (tolerance 1e-10)
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!A2") {
assert!((v - 1.016_666_666_7).abs() < 1e-10);
} else {
panic!("Expected numeric value in A2");
}
// always positive A1
assert_eq!(model._get_text("A3"), *"1");
}
#[test]
fn test_yearfrac_basis_3_actual_365() {
let mut model = new_empty_model();
// Non-leap span of exactly 365 days should result in 1.0
model._set("B1", "=YEARFRAC(44561,44926,3)");
// Leap-year span of 366 days
model._set("B2", "=YEARFRAC(43831,44197,3)");
// Same date should be 0
model._set("B3", "=YEARFRAC(44561,44561,3)");
model.evaluate();
// 365/365
assert_eq!(model._get_text("B1"), *"1");
// 366/365 ≈ 1.002739726 (tolerance 1e-10)
if let Ok(CellValue::Number(v)) = model.get_cell_value_by_ref("Sheet1!B2") {
assert!((v - 1.002_739_726).abs() < 1e-10);
} else {
panic!("Expected numeric value in B2");
}
// Same date
assert_eq!(model._get_text("B3"), *"0");
}

View File

@@ -89,67 +89,3 @@ fn clear_all_empty_cell() {
model.undo().unwrap(); model.undo().unwrap();
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string())); assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
} }
#[test]
fn issue_454() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_user_input(
0,
1,
1,
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
)
.unwrap();
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("TRUE".to_string())
);
model
.range_clear_contents(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("FALSE".to_string())
);
model.undo().unwrap();
}
#[test]
fn issue_454b() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model
.set_user_input(
0,
1,
1,
"Le presbytère n'a rien perdu de son charme, ni le jardin de son éclat.",
)
.unwrap();
model.set_user_input(0, 1, 2, "=ISTEXT(A1)").unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("TRUE".to_string())
);
model
.range_clear_all(&Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
})
.unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 1, 2),
Ok("FALSE".to_string())
);
model.undo().unwrap();
}

View File

@@ -254,19 +254,19 @@ fn invalid_names() {
// spaces // spaces
assert_eq!( assert_eq!(
model.new_defined_name("A real", None, "Sheet1!$A$1"), model.new_defined_name("A real", None, "Sheet1!$A$1"),
Err("Name: Invalid defined name".to_string()) Err("Invalid defined name".to_string())
); );
// Starts with number // Starts with number
assert_eq!( assert_eq!(
model.new_defined_name("2real", None, "Sheet1!$A$1"), model.new_defined_name("2real", None, "Sheet1!$A$1"),
Err("Name: Invalid defined name".to_string()) Err("Invalid defined name".to_string())
); );
// Updating also fails // Updating also fails
assert_eq!( assert_eq!(
model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"), model.update_defined_name("MyName", None, "My Name", None, "Sheet1!$A$1"),
Err("Name: Invalid defined name".to_string()) Err("Invalid defined name".to_string())
); );
} }
@@ -284,13 +284,13 @@ fn already_existing() {
// Can't create a new name with the same name // Can't create a new name with the same name
assert_eq!( assert_eq!(
model.new_defined_name("MyName", None, "Sheet1!$A$2"), model.new_defined_name("MyName", None, "Sheet1!$A$2"),
Err("Name: Defined name already exists".to_string()) Err("Defined name already exists".to_string())
); );
// Can't update one into an existing // Can't update one into an existing
assert_eq!( assert_eq!(
model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"), model.update_defined_name("Another", None, "MyName", None, "Sheet1!$A$1"),
Err("Name: Defined name already exists".to_string()) Err("Defined name already exists".to_string())
); );
} }
@@ -304,17 +304,17 @@ fn invalid_sheet() {
assert_eq!( assert_eq!(
model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"), model.new_defined_name("Mything", Some(2), "Sheet1!$A$1"),
Err("Scope: Invalid sheet index".to_string()) Err("Invalid sheet index".to_string())
); );
assert_eq!( assert_eq!(
model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"), model.update_defined_name("MyName", None, "MyName", Some(2), "Sheet1!$A$1"),
Err("Scope: Invalid sheet index".to_string()) Err("Invalid sheet index".to_string())
); );
assert_eq!( assert_eq!(
model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"), model.update_defined_name("MyName", Some(9), "YourName", None, "Sheet1!$A$1"),
Err("General: Failed to get old name".to_string()) Err("Invalid sheet index".to_string())
); );
} }
@@ -322,7 +322,7 @@ fn invalid_sheet() {
fn invalid_formula() { fn invalid_formula() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap(); let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "Hello").unwrap(); model.set_user_input(0, 1, 1, "Hello").unwrap();
assert!(model.new_defined_name("MyName", None, "A1").is_err()); model.new_defined_name("MyName", None, "A1").unwrap();
model.set_user_input(0, 1, 2, "=MyName").unwrap(); model.set_user_input(0, 1, 2, "=MyName").unwrap();

View File

@@ -445,13 +445,11 @@ impl Default for Fill {
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum HorizontalAlignment { pub enum HorizontalAlignment {
Center, Center,
CenterContinuous, CenterContinuous,
Distributed, Distributed,
Fill, Fill,
#[default]
General, General,
Justify, Justify,
Left, Left,
@@ -459,6 +457,11 @@ pub enum HorizontalAlignment {
} }
// Note that alignment in "General" depends on type // Note that alignment in "General" depends on type
impl Default for HorizontalAlignment {
fn default() -> Self {
Self::General
}
}
impl HorizontalAlignment { impl HorizontalAlignment {
fn is_default(&self) -> bool { fn is_default(&self) -> bool {
@@ -484,9 +487,7 @@ impl Display for HorizontalAlignment {
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum VerticalAlignment { pub enum VerticalAlignment {
#[default]
Bottom, Bottom,
Center, Center,
Distributed, Distributed,
@@ -500,6 +501,12 @@ impl VerticalAlignment {
} }
} }
impl Default for VerticalAlignment {
fn default() -> Self {
Self::Bottom
}
}
impl Display for VerticalAlignment { impl Display for VerticalAlignment {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {

View File

@@ -329,7 +329,6 @@ impl Model {
Function::Tbillyield => self.units_fn_percentage_2(args, cell), Function::Tbillyield => self.units_fn_percentage_2(args, cell),
Function::Date => self.units_fn_dates(args, cell), Function::Date => self.units_fn_dates(args, cell),
Function::Today => self.units_fn_dates(args, cell), Function::Today => self.units_fn_dates(args, cell),
Function::Now => self.units_fn_date_times(args, cell),
_ => None, _ => None,
} }
} }
@@ -376,8 +375,4 @@ impl Model {
// TODO: update locale and use it here // TODO: update locale and use it here
Some(Units::Date("dd/mm/yyyy".to_string())) Some(Units::Date("dd/mm/yyyy".to_string()))
} }
fn units_fn_date_times(&self, _args: &[Node], _cell: &CellReferenceIndex) -> Option<Units> {
Some(Units::Date("dd/mm/yyyy hh:mm:ss".to_string()))
}
} }

View File

@@ -627,7 +627,6 @@ impl UserModel {
} }
} }
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -657,7 +656,6 @@ impl UserModel {
} }
} }
self.push_diff_list(diff_list); self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -2001,10 +1999,7 @@ impl UserModel {
new_scope: Option<u32>, new_scope: Option<u32>,
new_formula: &str, new_formula: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let old_formula = self let old_formula = self.model.get_defined_name_formula(name, scope)?;
.model
.get_defined_name_formula(name, scope)
.map_err(|_| "General: Failed to get old name")?;
let diff_list = vec![Diff::UpdateDefinedName { let diff_list = vec![Diff::UpdateDefinedName {
name: name.to_string(), name: name.to_string(),
scope, scope,
@@ -2020,16 +2015,6 @@ impl UserModel {
Ok(()) Ok(())
} }
/// validates a new defined name
pub fn is_valid_defined_name(
&self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<Option<u32>, String> {
self.model.is_valid_defined_name(name, scope, formula)
}
// **** Private methods ****** // // **** Private methods ****** //
pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) { pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {

View File

@@ -1,7 +1,7 @@
[package] [package]
edition = "2021" edition = "2021"
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.6.0" version = "0.5.0"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] } napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] }
napi-derive = "2.12.2" napi-derive = "2.12.2"
ironcalc = { path = "../../xlsx", version = "0.6.0" } ironcalc = { path = "../../xlsx", version = "0.5.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[build-dependencies] [build-dependencies]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pyroncalc" name = "pyroncalc"
version = "0.6.0" version = "0.5.7"
edition = "2021" edition = "2021"
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.6.0" } xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
pyo3 = { version = "0.25", features = ["extension-module"] } pyo3 = { version = "0.25", features = ["extension-module"] }
bitcode = "0.6.3" bitcode = "0.6.3"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "ironcalc" name = "ironcalc"
version = "0.6.0" version = "0.5.7"
description = "Create, edit and evaluate Excel spreadsheets" description = "Create, edit and evaluate Excel spreadsheets"
requires-python = ">=3.10" requires-python = ">=3.10"
keywords = [ keywords = [

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "wasm" name = "wasm"
version = "0.6.0" version = "0.5.0"
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
description = "IronCalc Web bindings" description = "IronCalc Web bindings"
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
# Uses `../ironcalc/base` when used locally, and uses # Uses `../ironcalc/base` when used locally, and uses
# the inicated version from crates.io when published. # the inicated version from crates.io when published.
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../../base", version = "0.6", features = ["use_regex_lite"] } ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2.100" wasm-bindgen = "0.2.100"
serde-wasm-bindgen = "0.4" serde-wasm-bindgen = "0.4"

View File

@@ -5,11 +5,7 @@ use wasm_bindgen::{
}; };
use ironcalc_base::{ use ironcalc_base::{
expressions::{ expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
lexer::util::get_tokens as tokenizer,
types::Area,
utils::{number_to_column, quote_name as quote_name_ic},
},
types::{CellType, Style}, types::{CellType, Style},
worksheet::NavigationDirection, worksheet::NavigationDirection,
BorderArea, ClipboardData, UserModel as BaseModel, BorderArea, ClipboardData, UserModel as BaseModel,
@@ -35,11 +31,6 @@ pub fn column_name_from_number(column: i32) -> Result<String, JsError> {
} }
} }
#[wasm_bindgen(js_name = "quoteName")]
pub fn quote_name(name: &str) -> String {
quote_name_ic(name)
}
#[derive(Serialize)] #[derive(Serialize)]
struct DefinedName { struct DefinedName {
name: String, name: String,
@@ -775,17 +766,4 @@ impl Model {
.get_first_non_empty_in_row_after_column(sheet, row, column) .get_first_non_empty_in_row_after_column(sheet, row, column)
.map_err(to_js_error) .map_err(to_js_error)
} }
#[wasm_bindgen(js_name = "isValidDefinedName")]
pub fn is_valid_defined_name(
&self,
name: &str,
scope: Option<u32>,
formula: &str,
) -> Result<(), JsError> {
match self.model.is_valid_defined_name(name, scope, formula) {
Ok(_) => Ok(()),
Err(e) => Err(to_js_error(e.to_string())),
}
}
} }

View File

@@ -1,14 +0,0 @@
services:
server:
image: ghcr.io/ironcalc/ironcalc-server:0.6.0
build:
context: .
target: server-runtime
caddy:
image: ghcr.io/ironcalc/ironcalc-caddy:0.6.0
build:
context: .
target: caddy-runtime
ports:
- "2080:2080"

1182
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
}, },
"devDependencies": { "devDependencies": {
"markdown-it-mathjax3": "^4.3.2", "markdown-it-mathjax3": "^4.3.2",
"vitepress": "^v2.0.0-alpha.12", "vitepress": "^v2.0.0-alpha.8",
"vue": "^3.5.17" "vue": "^3.5.17"
} }
} }

View File

@@ -2036,10 +2036,6 @@ export default defineConfig({
text: "How to contribute", text: "How to contribute",
link: "/contributing/how-to-contribute", link: "/contributing/how-to-contribute",
}, },
{
text: "Function documentation guide",
link: "/contributing/function-documentation-guide",
},
], ],
}, },
], ],

View File

@@ -1,389 +0,0 @@
---
layout: doc
outline: deep
lang: en-US
---
# Function Documentation Guide
This guide explains how to document IronCalc functions following our established format and style conventions.
## File Structure
Function documentation files should be placed in the appropriate category directory under `src/functions/`. For example:
- Financial functions: `src/functions/financial/function-name.md`
- Text functions: `src/functions/text/function-name.md`
- Logical functions: `src/functions/logical/function-name.md`
## Required Frontmatter
Every function documentation file must start with this frontmatter:
```yaml
---
layout: doc
outline: deep
lang: en-US
---
```
## Document Structure
A complete function documentation should include the following sections in order:
### 1. Title
The title should be the function name followed by the word "function":
```markdown
# FV function
```
The function name should be written in uppercase when mentioned in the documentation.
### 2. Draft Warning (Optional)
If the function hasn't been implemented, include this warning box:
```markdown
::: warning
**Note:** This draft page is under construction 🚧
:::
```
If the function has been implemented but not documented, include this warning box:
```markdown
::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb).
:::
```
### 3. Overview
Provide a brief, clear description of what the function does. If the function name is an acronym, expand it using underlined text:
```markdown
## Overview
FV (<u>F</u>uture <u>V</u>alue) is a function of the Financial category that can be used to predict the future value of an investment or asset based on its present value.
```
Include:
- Category (Financial, Text, Logical, etc.)
- Primary purpose
- Key use cases (if helpful)
### 4. Usage
This section contains multiple subsections:
#### 4.1 Syntax
Format the function syntax with color-coded argument types. Use the following color scheme:
- **Numbers**: `#2F80ED` (blue)
- **Booleans**: `#27AE60` (green)
- **Text/Strings**: `#2F80ED` (orange)
- **Arrays/Ranges**: `#EB5757` (red)
**Format:**
```markdown
### Syntax
**FUNCTION_NAME(<span title="Type" style="color:#HEXCODE">arg1</span>, <span title="Type" style="color:#HEXCODE">arg2</span>=default, ...) => <span title="ReturnType" style="color:#HEXCODE">return_value</span>**
```
**Example:**
**FV(<span title="Number" style="color:#2F80ED">rate</span>, <span title="Number" style="color:#2F80ED">nper</span>, <span title="Number" style="color:#2F80ED">pmt</span>, [<span title="Number" style="color:#2F80ED">pv</span>], [<span title="Boolean" style="color:#27AE60">type</span>] => <span title="Number" style="color:#2F80ED">fv</span>**
```markdown
### Syntax
**FV(<span title="Number" style="color:#2F80ED">rate</span>, <span title="Number" style="color:#2F80ED">nper</span>, <span title="Number" style="color:#2F80ED">pmt</span>, <span title="Number" style="color:#2F80ED">pv</span>=0, <span title="Boolean" style="color:#27AE60">type</span>=FALSE) => <span title="Number" style="color:#2F80ED">fv</span>**
```
**Guidelines:**
- Use `title` attribute to specify the data type
- Use `style="color:#HEXCODE"` for syntax highlighting
- Use square brackets for optional arguments
- Show the return type after `=>`
- Make the entire syntax **bold**
#### 4.2 Argument Descriptions
List each argument with:
- Argument name in _italics_
- Data type link (e.g., `[number](/features/value-types#numbers)`)
- Required or optional indicator
- Description
**Format:**
```markdown
### Argument descriptions
- _argname_ ([datatype](/features/value-types#datatype), [required|optional](/features/optional-arguments.md)). Description of the argument.
```
**Example:**
```markdown
### Argument descriptions
- _rate_ ([number](/features/value-types#numbers), required). The fixed percentage interest rate or yield per period.
- _pv_ ([number](/features/value-types#numbers), [optional](/features/optional-arguments.md)). "pv" is the <u>p</u>resent <u>v</u>alue or starting amount of the asset (default 0).
- _type_ ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). A logical value indicating whether the payment due dates are at the end (FALSE or 0) of the compounding periods or at the beginning (TRUE or any non-zero value). The default is FALSE when omitted.
```
**Guidelines:**
- Use bullet points (`*`)
- Italicize argument names with `*argname*`
- Link to value types documentation
- Link to optional arguments page when applicable
- Expand acronyms in descriptions using `<u>` tags if helpful
- Mention default values for optional arguments
#### 4.3 Additional Guidance
Provide tips, best practices and important notes about using the function:
```markdown
### Additional guidance
- Make sure that the _rate_ argument specifies the interest rate or yield applicable to the compounding period.
- The _pmt_ and _pv_ arguments should be expressed in the same currency unit.
- To ensure a worthwhile result, one of the _pmt_ and _pv_ arguments should be non-zero.
```
#### 4.4 Returned Value
Describe what the function returns:
```markdown
### Returned value
FV returns a [number](/features/value-types#numbers) representing the future value expressed in the same [currency unit](/features/units) that was used for the _pmt_ and _pv_ arguments.
```
Include:
- Return type (with link to value types if applicable)
- Units or format if relevant
- Any important characteristics
#### 4.5 Error Conditions
List all error scenarios the function may encounter:
```markdown
### Error conditions
- In common with many other IronCalc functions, FV propagates errors that are found in any of its arguments.
- If too few or too many arguments are supplied, FV returns the [`#ERROR!`](/features/error-types.md#error) error.
- If the value of any of the _rate_, _nper_, _pmt_ or _pv_ arguments is not (or cannot be converted to) a [number](/features/value-types#numbers), then FV returns the [`#VALUE!`](/features/error-types.md#value) error.
- If the value of the _type_ argument is not (or cannot be converted to) a [Boolean](/features/value-types#booleans), then FV again returns the [`#VALUE!`](/features/error-types.md#value) error.
- For some combinations of valid argument values, FV may return a [`#NUM!`](/features/error-types.md#num) error or a [`#DIV/0!`](/features/error-types.md#div-0) error.
```
**Guidelines:**
- Use bullet points
- Format error types using backticks and link to the error types page: `` [`#ERROR!`](/features/error-types.md#error) ``
- Reference argument names in italics when discussing specific arguments
- Add the include directive at the end if using the error details snippet:
```markdown
<!--@include: ../markdown-snippets/error-type-details.txt-->
```
### 5. Details (Optional but Recommended)
For functions with mathematical formulas or complex behavior, include a Details section. This section can also include plots, graphs or charts to help clarify the function's behavior.
```markdown
## Details
- If $\text{type} \neq 0$, $\text{fv}$ is given by the equation:
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$
- If $\text{type} = 0$, $\text{fv}$ is given by the equation:
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$
```
**Guidelines:**
- Use LaTeX math notation with `$` for inline and `$$` for block equations
- Use `\text{}` for variable names in equations
- Explain special cases or edge conditions
### 6. Examples
Link to interactive examples in IronCalc:
```markdown
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=functionname).
```
Replace `functionname` with the actual function name (lowercase).
### 7. Links
Provide external references and related functions:
```markdown
## Links
- For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.
- See also IronCalc's [NPER](/functions/financial/nper), [PMT](/functions/financial/pmt), [PV](/functions/financial/pv) and [RATE](/functions/financial/rate) functions.
- Visit Microsoft Excel's [FV function](https://support.microsoft.com/en-gb/office/fv-function-2eef9f44-a084-4c61-bdd8-4fe4bb1b71b3) page.
- Both [Google Sheets](https://support.google.com/docs/answer/3093224) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/FV) provide versions of the FV function.
```
**Guidelines:**
- Include Wikipedia links for concepts when available
- Link to related IronCalc functions in the same category
- Include links to equivalent functions in Excel, Google Sheets, and LibreOffice Calc
- Use bullet points
## Syntax Coloring Reference
### Color Codes
| Data Type | Hex Color | Usage |
| ----------- | --------- | --------------------------------------- |
| Number | `#2F80ED` | All numeric arguments and return values |
| Boolean | `#27AE60` | TRUE/FALSE arguments |
| Text/String | `#F2994A` | Text arguments |
| Array/Range | `#EB5757` | Array or range arguments |
### Syntax Highlighting Template
```html
<span title="Type" style="color:#HEXCODE">argument_name</span>
```
**Examples:**
- Number: `<span title="Number" style="color:#2F80ED">rate</span>`
- Boolean: `<span title="Boolean" style="color:#27AE60">type</span>`
- Text: `<span title="Text" style="color:#F2994A">text</span>`
## Formatting Conventions
### Text Formatting
- **Function names**: Use exact case as in IronCalc
- **Argument names**: Use _italics_ when referencing in prose
- **Acronyms**: Expand using `<u>` tags: `<u>F</u>uture <u>V</u>alue`
- **Code/values**: Use backticks for error codes: `` `#ERROR!` ``
- **Links**: Use descriptive link text, not raw URLs
### Section Headers
- Use `#` for the page title with the function name (e.g., FV Function)
- Use `##` for main sections (Overview, Usage, Details, Examples, Links)
- Use `###` for subsections (Syntax, Argument descriptions, etc.)
### Lists
- Use bullet points (`*`) for argument descriptions and error conditions
- Use numbered lists only when order matters
## Checklist
Before submitting a function documentation, ensure:
- [ ] Frontmatter is correct
- [ ] Title follows the format "FUNCTION_NAME function"
- [ ] Overview clearly explains the function's purpose
- [ ] Syntax is color-coded correctly
- [ ] All arguments are documented with correct types
- [ ] Required vs optional arguments are clearly marked
- [ ] Return value is described
- [ ] Error conditions are comprehensive
- [ ] Examples link is included
- [ ] Links section includes relevant references
- [ ] Mathematical formulas (if any) use proper LaTeX syntax
- [ ] All internal links use relative paths
- [ ] Spelling and grammar are correct
## Example Template
```markdown
---
layout: doc
outline: deep
lang: en-US
---
# FUNCTION_NAME function
::: warning
**Note:** This draft page is under construction 🚧
:::
## Overview
FUNCTION_NAME (<u>A</u>cronym <u>E</u>xplanation) is a function of the [Category] category that can be used to [primary purpose].
[Additional context about when to use this function or related functions.]
## Usage
### Syntax
**FUNCTION_NAME(<span title="Type" style="color:#2F80ED">arg1</span>, [<span title="Type" style="color:#2F80ED">arg2</span>], [<span title="Boolean" style="color:#27AE60">arg3</span>]) => <span title="Type" style="color:#2F80ED">return_value</span>**
### Argument descriptions
- _arg1_ ([type](/features/value-types#type), required). Description.
- _arg2_ ([type](/features/value-types#type), [optional](/features/optional-arguments.md)). Description (default value).
- _arg3_ ([Boolean](/features/value-types#booleans), [optional](/features/optional-arguments.md)). Description (default FALSE).
### Additional guidance
- Tip or best practice.
- Another important note.
### Returned value
FUNCTION_NAME returns a [type](/features/value-types#type) representing [description].
### Error conditions
- General error propagation note.
- Specific error condition with [`#ERROR!`](/features/error-types.md#error) link.
- Another error condition.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
[Mathematical formulas or detailed explanations if needed]
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=functionname).
## Links
- Wikipedia link if applicable.
- Related IronCalc functions.
- Microsoft Excel documentation.
- Google Sheets and LibreOffice Calc links.
```
## Questions?
If you have questions about documenting functions, reach out on our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or check existing function documentation for examples.

View File

@@ -6,10 +6,4 @@ lang: en-US
# How to Contribute # How to Contribute
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action) we've marked some issues there with the tag 'good first issue' that could serve you as a starting point. If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action), join our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
If you also want to discuss topics, share your thoughts or just say 'hi', you can join our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
## Documentation
If you're interested in contributing to our documentation, especially function documentation, please see our [Function Documentation Guide](/contributing/function-documentation-guide).

View File

@@ -68,8 +68,6 @@ Using IronCalc, a complex number is a string of the form "1+j3".
## Arrays ## Arrays
## Ranges
## References ## References
A reference is a pointer to a single cell or a range of cells. The reference can either be entered manually, for example "A4", or as the result of a calculation, such as the OFFSET Function or the INDIRECT Function. A reference can also be built, for example with the Colon (\:) Operator. A reference is a pointer to a single cell or a range of cells. The reference can either be entered manually, for example "A4", or as the result of a calculation, such as the OFFSET Function or the INDIRECT Function. A reference can also be built, for example with the Colon (\:) Operator.

View File

@@ -6,32 +6,33 @@ lang: en-US
# Date and Time functions # Date and Time functions
All Date and Time functions are already supported in IronCalc. At the moment IronCalc only supports a few function in this section.
You can track the progress in this [GitHub issue](https://github.com/ironcalc/IronCalc/issues/48).
| Function | Status | Documentation | | Function | Status | Documentation |
| ---------------- | ---------------------------------------------- | ------------- | | ---------------- | ---------------------------------------------- | ------------- |
| DATE | <Badge type="tip" text="Available" /> | | | DATE | <Badge type="tip" text="Available" /> | |
| DATEDIF | <Badge type="tip" text="Available" /> | | | DATEDIF | <Badge type="info" text="Not implemented yet" /> | |
| DATEVALUE | <Badge type="tip" text="Available" /> | [DATEVALUE](date_and_time/datevalue) | | DATEVALUE | <Badge type="info" text="Not implemented yet" /> | |
| DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) | | DAY | <Badge type="tip" text="Available" /> | [DAY](date_and_time/day) |
| DAYS | <Badge type="tip" text="Available" /> | | | DAYS | <Badge type="info" text="Not implemented yet" /> | |
| DAYS360 | <Badge type="tip" text="Available" /> | | | DAYS360 | <Badge type="info" text="Not implemented yet" /> | |
| EDATE | <Badge type="tip" text="Available" /> | | | EDATE | <Badge type="tip" text="Available" /> | |
| EOMONTH | <Badge type="tip" text="Available" /> | | | EOMONTH | <Badge type="tip" text="Available" /> | |
| HOUR | <Badge type="tip" text="Available" /> | | | HOUR | <Badge type="info" text="Not implemented yet" /> | |
| ISOWEEKNUM | <Badge type="tip" text="Available" /> | | | ISOWEEKNUM | <Badge type="info" text="Not implemented yet" /> | |
| MINUTE | <Badge type="tip" text="Available" /> | | | MINUTE | <Badge type="info" text="Not implemented yet" /> | |
| MONTH | <Badge type="tip" text="Available" /> | [MONTH](date_and_time/month) | | MONTH | <Badge type="tip" text="Available" /> | [MONTH](date_and_time/month) |
| NETWORKDAYS | <Badge type="tip" text="Available" /> | [NETWORKDAYS](date_and_time/networkdays) | | NETWORKDAYS | <Badge type="info" text="Not implemented yet" /> | |
| NETWORKDAYS.INTL | <Badge type="tip" text="Available" /> | [NETWORKDAYS.INTL](date_and_time/networkdays.intl) | | NETWORKDAYS.INTL | <Badge type="info" text="Not implemented yet" /> | |
| NOW | <Badge type="tip" text="Available" /> | | | NOW | <Badge type="tip" text="Available" /> | |
| SECOND | <Badge type="tip" text="Available" /> | | | SECOND | <Badge type="info" text="Not implemented yet" /> | |
| TIME | <Badge type="tip" text="Available" /> | | | TIME | <Badge type="info" text="Not implemented yet" /> | |
| TIMEVALUE | <Badge type="tip" text="Available" /> | [TIMEVALUE](date_and_time/timevalue) | | TIMEVALUE | <Badge type="info" text="Not implemented yet" /> | |
| TODAY | <Badge type="tip" text="Available" /> | | | TODAY | <Badge type="tip" text="Available" /> | |
| WEEKDAY | <Badge type="tip" text="Available" /> | | | WEEKDAY | <Badge type="info" text="Not implemented yet" /> | |
| WEEKNUM | <Badge type="tip" text="Available" /> | | | WEEKNUM | <Badge type="info" text="Not implemented yet" /> | |
| WORKDAY | <Badge type="tip" text="Available" /> | | | WORKDAY | <Badge type="info" text="Not implemented yet" /> | |
| WORKDAY.INTL | <Badge type="tip" text="Available" /> | | | WORKDAY.INTL | <Badge type="info" text="Not implemented yet" /> | |
| YEAR | <Badge type="tip" text="Available" /> | [YEAR](date_and_time/year) | | YEAR | <Badge type="tip" text="Available" /> | [YEAR](date_and_time/year) |
| YEARFRAC | <Badge type="tip" text="Available" /> | | | YEARFRAC | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -7,5 +7,6 @@ lang: en-US
# DATEDIF # DATEDIF
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -4,41 +4,9 @@ outline: deep
lang: en-US lang: en-US
--- ---
# DATEVALUE function # DATEVALUE
## Overview ::: warning
DATEVALUE is a function of the Date and Time category that converts a date stored as text to a [serial number](/features/serial-numbers.md) corresponding to a date value. 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
## Usage :::
### Syntax
**DATEVALUE(<span title="Text" style="color:#1E88E5">date_text</span>) => <span title="Number" style="color:#1E88E5">datevalue</span>**
### Argument descriptions
* *date_text* ([text](/features/value-types#strings), required). A text string that represents a date in a known format. The text must represent a date between December 31, 1899 and December 31, 9999.
### Additional guidance
* If the year portion of the *date_text* argument is omitted, DATEVALUE uses the current year from the system clock.
* Time information in the *date_text* argument is ignored. DATEVALUE processes only the date portion.
### Returned value
DATEVALUE returns a [number](/features/value-types#numbers) that represents the date as a [serial number](/features/serial-numbers.md). The serial number corresponds to the number of days since December 31, 1899.
### Error conditions
* In common with many other IronCalc functions, DATEVALUE propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then DATEVALUE returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *date_text* argument is not (or cannot be converted to) a [text](/features/value-types#strings) value, then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *date_text* argument represents a date outside the valid range (before December 31, 1899 or after December 31, 9999), then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *date_text* argument cannot be recognized as a valid date format, then DATEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!-- ## Details
For more information on how IronCalc processes Date and Time functions and values, visit [Date and Time](/features/serial-numbers.md)
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=datevalue).
-->
## Links
* See also IronCalc's [TIMEVALUE](/functions/date_and_time/timevalue.md) function for converting time text to serial numbers.
* Visit Microsoft Excel's [DATEVALUE function](https://support.microsoft.com/en-us/office/datevalue-function-df8b07d4-7761-4a93-bc33-b7471bbff252) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093039) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/DATEVALUE) provide versions of the DATEVALUE function.

View File

@@ -7,5 +7,6 @@ lang: en-US
# DAYS # DAYS
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# DAYS360 # DAYS360
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,6 +7,6 @@ lang: en-US
# HOUR # HOUR
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
The HOUR function is implemented and available in IronCalc. [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# ISOWEEKNUM # ISOWEEKNUM
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,6 +7,6 @@ lang: en-US
# MINUTE # MINUTE
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
The MINUTE function is implemented and available in IronCalc. [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -4,73 +4,9 @@ outline: deep
lang: en-US lang: en-US
--- ---
# NETWORKDAYS.INTL function # NETWORKDAYS.INTL
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
::: [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
## Overview
NETWORKDAYS.INTL is a function of the Date and Time category that calculates the number of working days between two dates, with customizable weekend definitions and optionally specified holidays.
## Usage
### Syntax
**NETWORKDAYS.INTL(<span title="Number" style="color:#1E88E5">start_date</span>, <span title="Number" style="color:#1E88E5">end_date</span>, [<span title="Number or String" style="color:#4CAF50">weekend</span>], [<span title="Array" style="color:#E91E63">holidays</span>]) => <span title="Number" style="color:#1E88E5">workdays</span>**
### Argument descriptions
* *start_date* ([number](/features/value-types#numbers), required). The start date expressed as a [serial number](/features/serial-numbers.md).
* *end_date* ([number](/features/value-types#numbers), required). The end date expressed as a [serial number](/features/serial-numbers.md).
* *weekend* ([number](/features/value-types#numbers) or [string](/features/value-types#strings), optional). Defines which days are considered weekends. Default is 1 (Saturday-Sunday).
* *holidays* ([array](/features/value-types#arrays) or [range](/features/value-types#ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
### Weekend parameter options
The _weekend_ parameter can be specified in two ways:
**Numeric codes:**
- 1 (default): Saturday and Sunday
- 2: Sunday and Monday
- 3: Monday and Tuesday
- 4: Tuesday and Wednesday
- 5: Wednesday and Thursday
- 6: Thursday and Friday
- 7: Friday and Saturday
- 11: Sunday only
- 12: Monday only
- 13: Tuesday only
- 14: Wednesday only
- 15: Thursday only
- 16: Friday only
- 17: Saturday only
**String pattern:** A 7-character string of "0" and "1" where "1" indicates a weekend day. The string represents Monday through Sunday. For example, "0000011" means Saturday and Sunday are weekends.
### Additional guidance
- If the supplied _start_date_ and _end_date_ arguments have fractional parts, NETWORKDAYS.INTL uses their [floor values](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions).
- If _start_date_ is later than _end_date_, the function returns a negative number.
- Empty cells in the _holidays_ array are ignored.
- The calculation includes both the start and end dates if they are workdays.
### Returned value
NETWORKDAYS.INTL returns a [number](/features/value-types#numbers) representing the count of working days between the two dates.
### Error conditions
* In common with many other IronCalc functions, NETWORKDAYS.INTL propagates errors that are found in its arguments.
* If fewer than 2 or more than 4 arguments are supplied, then NETWORKDAYS.INTL returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the *start_date* or *end_date* arguments are not (or cannot be converted to) [numbers](/features/value-types#numbers), then NETWORKDAYS.INTL returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *start_date* or *end_date* values are outside the valid date range, then NETWORKDAYS.INTL returns the [`#NUM!`](/features/error-types.md#num) error.
* If the *weekend* parameter is an invalid numeric code or an improperly formatted string, then NETWORKDAYS.INTL returns the [`#NUM!`](/features/error-types.md#num) or [`#VALUE!`](/features/error-types.md#value) error.
* If the *holidays* array contains non-numeric values, then NETWORKDAYS.INTL returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
IronCalc utilizes Rust's [chrono](https://docs.rs/chrono/latest/chrono/) crate to implement the NETWORKDAYS.INTL function. This function provides more flexibility than NETWORKDAYS by allowing custom weekend definitions.
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=networkdays-intl).
## Links
* See also IronCalc's [NETWORKDAYS](/functions/date_and_time/networkdays.md) function for the basic version with fixed weekends.
* Visit Microsoft Excel's [NETWORKDAYS.INTL function](https://support.microsoft.com/en-us/office/networkdays-intl-function-a9b26239-4f20-46a1-9ab8-4e925bfd5e28) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093019) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/NETWORKDAYS.INTL) provide versions of the NETWORKDAYS.INTL function.

View File

@@ -4,51 +4,9 @@ outline: deep
lang: en-US lang: en-US
--- ---
# NETWORKDAYS function # NETWORKDAYS
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
::: [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
:::
## Overview
NETWORKDAYS is a function of the Date and Time category that calculates the number of working days between two dates, excluding weekends (Saturday and Sunday by default) and optionally specified holidays.
## Usage
### Syntax
**NETWORKDAYS(<span title="Number" style="color:#1E88E5">start_date</span>, <span title="Number" style="color:#1E88E5">end_date</span>, [<span title="Array" style="color:#E91E63">holidays</span>]) => <span title="Number" style="color:#1E88E5">workdays</span>**
### Argument descriptions
* *start_date* ([number](/features/value-types#numbers), required). The start date expressed as a [serial number](/features/serial-numbers.md).
* *end_date* ([number](/features/value-types#numbers), required). The end date expressed as a [serial number](/features/serial-numbers.md).
* *holidays* ([array](/features/value-types#arrays) or [range](/features/value-types#ranges), optional). A list of dates to exclude from the calculation, expressed as serial numbers.
### Additional guidance
- If the supplied _start_date_ and _end_date_ arguments have fractional parts, NETWORKDAYS uses their [floor values](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions).
- If _start_date_ is later than _end_date_, the function returns a negative number.
- Weekend days are Saturday and Sunday by default. Use [NETWORKDAYS.INTL](networkdays.intl) for custom weekend definitions.
- Empty cells in the _holidays_ array are ignored.
- The calculation includes both the start and end dates if they are workdays.
### Returned value
NETWORKDAYS returns a [number](/features/value-types#numbers) representing the count of working days between the two dates.
### Error conditions
* In common with many other IronCalc functions, NETWORKDAYS propagates errors that are found in its arguments.
* If fewer than 2 or more than 3 arguments are supplied, then NETWORKDAYS returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the *start_date* or *end_date* arguments are not (or cannot be converted to) [numbers](/features/value-types#numbers), then NETWORKDAYS returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *start_date* or *end_date* values are outside the valid date range, then NETWORKDAYS returns the [`#NUM!`](/features/error-types.md#num) error.
* If the *holidays* array contains non-numeric values, then NETWORKDAYS returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
IronCalc utilizes Rust's [chrono](https://docs.rs/chrono/latest/chrono/) crate to implement the NETWORKDAYS function. The function treats Saturday and Sunday as weekend days.
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=networkdays).
## Links
* See also IronCalc's [NETWORKDAYS.INTL](/functions/date_and_time/networkdays.intl.md) function for custom weekend definitions.
* Visit Microsoft Excel's [NETWORKDAYS function](https://support.microsoft.com/en-us/office/networkdays-function-48e717bf-a7a3-495f-969e-5005e3eb18e7) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093018) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/NETWORKDAYS) provide versions of the NETWORKDAYS function.

View File

@@ -7,6 +7,6 @@ lang: en-US
# SECOND # SECOND
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
The SECOND function is implemented and available in IronCalc. [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,6 +7,6 @@ lang: en-US
# TIME # TIME
::: warning ::: warning
**Note:** This draft page is under construction 🚧 🚧 This function is not yet available in IronCalc.
The TIME function is implemented and available in IronCalc. [Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -4,42 +4,9 @@ outline: deep
lang: en-US lang: en-US
--- ---
# TIMEVALUE function # TIMEVALUE
## Overview ::: warning
TIMEVALUE is a function of the Date and Time category that converts a time stored as text to a [serial number](/features/serial-numbers.md) corresponding to a time value. The serial number represents time as a decimal fraction of a 24-hour day (e.g., 0.5 represents 12:00:00 noon). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
## Usage :::
### Syntax
**TIMEVALUE(<span title="Text" style="color:#1E88E5">time_text</span>) => <span title="Number" style="color:#1E88E5">timevalue</span>**
### Argument descriptions
* *time_text* ([text](/features/value-types#strings), required). A text string that represents a time in a known format. The text must represent a time between 00:00:00 and 23:59:59.
### Additional guidance
* Date information in the *time_text* argument is ignored. TIMEVALUE processes only the time portion.
* The function can handle various time formats, including both 12-hour and 24-hour formats, as well as text that includes both date and time information.
### Returned value
TIMEVALUE returns a [number](/features/value-types#numbers) that represents the time as a [serial number](/features/serial-numbers.md). The serial number is a decimal fraction of a 24-hour day, where:
* 0.0 represents 00:00:00 (midnight)
* 0.5 represents 12:00:00 (midday)
* 0.99999... represents 23:59:59 (just before midnight)
### Error conditions
* In common with many other IronCalc functions, TIMEVALUE propagates errors that are found in its argument.
* If no argument, or more than one argument, is supplied, then TIMEVALUE returns the [`#ERROR!`](/features/error-types.md#error) error.
* If the value of the *time_text* argument is not (or cannot be converted to) a [text](/features/value-types#strings) value, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *time_text* argument represents a time outside the valid range, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
* If the *time_text* argument cannot be recognized as a valid time format, then TIMEVALUE returns the [`#VALUE!`](/features/error-types.md#value) error.
<!--@include: ../markdown-snippets/error-type-details.txt-->
<!--
## Examples
[See some examples in IronCalc](https://app.ironcalc.com/?example=timevalue).
-->
## Links
* See also IronCalc's [DATEVALUE](/functions/date_and_time/datevalue.md) function for converting date text to serial numbers.
* Visit Microsoft Excel's [TIMEVALUE function](https://support.microsoft.com/en-us/office/timevalue-function-0b615c12-33d8-4431-bf3d-f3eb6d186645) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3267350) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/TIMEVALUE) provide versions of the TIMEVALUE function.

View File

@@ -7,5 +7,6 @@ lang: en-US
# WEEKDAY # WEEKDAY
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# WEEKNUM # WEEKNUM
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# WORKDAY.INTL # WORKDAY.INTL
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# WORKDAY # WORKDAY
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -7,5 +7,6 @@ lang: en-US
# YEARFRAC # YEARFRAC
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

View File

@@ -14,10 +14,10 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| ABS | <Badge type="tip" text="Available" /> | | | ABS | <Badge type="tip" text="Available" /> | |
| ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) | | ACOS | <Badge type="tip" text="Available" /> | [ACOS](math_and_trigonometry/acos) |
| ACOSH | <Badge type="tip" text="Available" /> | [ACOSH](math_and_trigonometry/acosh) | | ACOSH | <Badge type="tip" text="Available" /> | [ACOSH](math_and_trigonometry/acosh) |
| ACOT | <Badge type="tip" text="Available" /> | | | ACOT | <Badge type="info" text="Not implemented yet" /> | |
| ACOTH | <Badge type="tip" text="Available" /> | | | ACOTH | <Badge type="info" text="Not implemented yet" /> | |
| AGGREGATE | <Badge type="info" text="Not implemented yet" /> | | | AGGREGATE | <Badge type="info" text="Not implemented yet" /> | |
| ARABIC | <Badge type="tip" text="Available" /> | | | ARABIC | <Badge type="info" text="Not implemented yet" /> | |
| ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) | | ASIN | <Badge type="tip" text="Available" /> | [ASIN](math_and_trigonometry/asin) |
| ASINH | <Badge type="tip" text="Available" /> | [ASINH](math_and_trigonometry/asinh) | | ASINH | <Badge type="tip" text="Available" /> | [ASINH](math_and_trigonometry/asinh) |
| ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) | | ATAN | <Badge type="tip" text="Available" /> | [ATAN](math_and_trigonometry/atan) |
@@ -27,64 +27,64 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| CEILING | <Badge type="info" text="Not implemented yet" /> | | | CEILING | <Badge type="info" text="Not implemented yet" /> | |
| CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | | | CEILING.MATH | <Badge type="info" text="Not implemented yet" /> | |
| CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | | | CEILING.PRECISE | <Badge type="info" text="Not implemented yet" /> | |
| COMBIN | <Badge type="tip" text="Available" /> | | | COMBIN | <Badge type="info" text="Not implemented yet" /> | |
| COMBINA | <Badge type="tip" text="Available" /> | | | COMBINA | <Badge type="info" text="Not implemented yet" /> | |
| COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) | | COS | <Badge type="tip" text="Available" /> | [COS](math_and_trigonometry/cos) |
| COSH | <Badge type="tip" text="Available" /> | [COSH](math_and_trigonometry/cosh) | | COSH | <Badge type="tip" text="Available" /> | [COSH](math_and_trigonometry/cosh) |
| COT | <Badge type="tip" text="Available" /> | | | COT | <Badge type="info" text="Not implemented yet" /> | |
| COTH | <Badge type="tip" text="Available" /> | | | COTH | <Badge type="info" text="Not implemented yet" /> | |
| CSC | <Badge type="tip" text="Available" /> | | | CSC | <Badge type="info" text="Not implemented yet" /> | |
| CSCH | <Badge type="tip" text="Available" /> | | | CSCH | <Badge type="info" text="Not implemented yet" /> | |
| DECIMAL | <Badge type="info" text="Not implemented yet" /> | | | DECIMAL | <Badge type="info" text="Not implemented yet" /> | |
| DEGREES | <Badge type="tip" text="Available" /> | [DEGREES](math_and_trigonometry/degrees) | | DEGREES | <Badge type="info" text="Not implemented yet" /> | |
| EVEN | <Badge type="tip" text="Available" /> | [EVEN](math_and_trigonometry/even) | | EVEN | <Badge type="info" text="Not implemented yet" /> | |
| EXP | <Badge type="tip" text="Available" /> | | | EXP | <Badge type="info" text="Not implemented yet" /> | |
| FACT | <Badge type="info" text="Not implemented yet" /> | | | FACT | <Badge type="info" text="Not implemented yet" /> | |
| FACTDOUBLE | <Badge type="info" text="Not implemented yet" /> | | | FACTDOUBLE | <Badge type="info" text="Not implemented yet" /> | |
| FLOOR | <Badge type="info" text="Not implemented yet" /> | | | FLOOR | <Badge type="info" text="Not implemented yet" /> | |
| FLOOR.MATH | <Badge type="info" text="Not implemented yet" /> | | | FLOOR.MATH | <Badge type="info" text="Not implemented yet" /> | |
| FLOOR.PRECISE | <Badge type="info" text="Not implemented yet" /> | | | FLOOR.PRECISE | <Badge type="info" text="Not implemented yet" /> | |
| GCD | <Badge type="info" text="Not implemented yet" /> | | | GCD | <Badge type="info" text="Not implemented yet" /> | |
| INT | <Badge type="tip" text="Available" /> | | | INT | <Badge type="info" text="Not implemented yet" /> | |
| ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | | | ISO.CEILING | <Badge type="info" text="Not implemented yet" /> | |
| LCM | <Badge type="info" text="Not implemented yet" /> | | | LCM | <Badge type="info" text="Not implemented yet" /> | |
| LET | <Badge type="info" text="Not implemented yet" /> | | | LET | <Badge type="info" text="Not implemented yet" /> | |
| LN | <Badge type="tip" text="Available" /> | | | LN | <Badge type="info" text="Available" /> | |
| LOG | <Badge type="tip" text="Available" /> | | | LOG | <Badge type="info" text="Available" /> | |
| LOG10 | <Badge type="tip" text="Available" /> | | | LOG10 | <Badge type="info" text="Available" /> | |
| MDETERM | <Badge type="info" text="Not implemented yet" /> | | | MDETERM | <Badge type="info" text="Not implemented yet" /> | |
| MINVERSE | <Badge type="info" text="Not implemented yet" /> | | | MINVERSE | <Badge type="info" text="Not implemented yet" /> | |
| MMULT | <Badge type="info" text="Not implemented yet" /> | | | MMULT | <Badge type="info" text="Not implemented yet" /> | |
| MOD | <Badge type="tip" text="Available" /> | [MOD](math_and_trigonometry/mod) | | MOD | <Badge type="info" text="Not implemented yet" /> | |
| MROUND | <Badge type="tip" text="Available" /> | | | MROUND | <Badge type="info" text="Not implemented yet" /> | |
| MULTINOMIAL | <Badge type="info" text="Not implemented yet" /> | | | MULTINOMIAL | <Badge type="info" text="Not implemented yet" /> | |
| MUNIT | <Badge type="info" text="Not implemented yet" /> | | | MUNIT | <Badge type="info" text="Not implemented yet" /> | |
| ODD | <Badge type="tip" text="Available" /> | [ODD](math_and_trigonometry/odd) | | ODD | <Badge type="info" text="Not implemented yet" /> | |
| PI | <Badge type="info" text="Not implemented yet" /> | | | PI | <Badge type="info" text="Not implemented yet" /> | |
| POWER | <Badge type="tip" text="Available" /> | | | POWER | <Badge type="tip" text="Available" /> | |
| PRODUCT | <Badge type="tip" text="Available" /> | | | PRODUCT | <Badge type="tip" text="Available" /> | |
| QUOTIENT | <Badge type="tip" text="Available" /> | [QUOTIENT](math_and_trigonometry/quotient) | | QUOTIENT | <Badge type="info" text="Not implemented yet" /> | |
| RADIANS | <Badge type="tip" text="Available" /> | [RADIANS](math_and_trigonometry/radians) | | RADIANS | <Badge type="info" text="Not implemented yet" /> | |
| RAND | <Badge type="tip" text="Available" /> | | | RAND | <Badge type="tip" text="Available" /> | |
| RANDARRAY | <Badge type="info" text="Not implemented yet" /> | | | RANDARRAY | <Badge type="info" text="Not implemented yet" /> | |
| RANDBETWEEN | <Badge type="tip" text="Available" /> | | | RANDBETWEEN | <Badge type="tip" text="Available" /> | |
| ROMAN | <Badge type="tip" text="Available" /> | | | ROMAN | <Badge type="info" text="Not implemented yet" /> | |
| ROUND | <Badge type="tip" text="Available" /> | | | ROUND | <Badge type="tip" text="Available" /> | |
| ROUNDDOWN | <Badge type="tip" text="Available" /> | | | ROUNDDOWN | <Badge type="tip" text="Available" /> | |
| ROUNDUP | <Badge type="tip" text="Available" /> | | | ROUNDUP | <Badge type="tip" text="Available" /> | |
| SEC | <Badge type="tip" text="Available" /> | | | SEC | <Badge type="info" text="Not implemented yet" /> | |
| SECH | <Badge type="tip" text="Available" /> | | | SECH | <Badge type="info" text="Not implemented yet" /> | |
| SERIESSUM | <Badge type="info" text="Not implemented yet" /> | | | SERIESSUM | <Badge type="info" text="Not implemented yet" /> | |
| SEQUENCE | <Badge type="info" text="Not implemented yet" /> | | | SEQUENCE | <Badge type="info" text="Not implemented yet" /> | |
| SIGN | <Badge type="tip" text="Available" /> | | | SIGN | <Badge type="info" text="Not implemented yet" /> | |
| SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) | | SIN | <Badge type="tip" text="Available" /> | [SIN](math_and_trigonometry/sin) |
| SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) | | SINH | <Badge type="tip" text="Available" /> | [SINH](math_and_trigonometry/sinh) |
| SQRT | <Badge type="tip" text="Available" /> | | | SQRT | <Badge type="tip" text="Available" /> | |
| SQRTPI | <Badge type="tip" text="Available" /> | | | SQRTPI | <Badge type="info" text="Available" /> | |
| SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | | | SUBTOTAL | <Badge type="info" text="Not implemented yet" /> | |
| SUM | <Badge type="tip" text="Available" /> | | | SUM | <Badge type="tip" text="Available" /> | |
| SUMIF | <Badge type="tip" text="Available" /> | | | SUMIF | <Badge type="tip" text="Available" /> | |
| SUMIFS | <Badge type="tip" text="Available" /> | | | SUMIFS | <Badge type="info" text="Available" /> | |
| SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | | | SUMPRODUCT | <Badge type="info" text="Not implemented yet" /> | |
| SUMSQ | <Badge type="info" text="Not implemented yet" /> | | | SUMSQ | <Badge type="info" text="Not implemented yet" /> | |
| SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | | | SUMX2MY2 | <Badge type="info" text="Not implemented yet" /> | |
@@ -92,4 +92,4 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | | | SUMXMY2 | <Badge type="info" text="Not implemented yet" /> | |
| TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) | | TAN | <Badge type="tip" text="Available" /> | [TAN](math_and_trigonometry/tan) |
| TANH | <Badge type="tip" text="Available" /> | [TANH](math_and_trigonometry/tanh) | | TANH | <Badge type="tip" text="Available" /> | [TANH](math_and_trigonometry/tanh) |
| TRUNC | <Badge type="tip" text="Available" /> | | | TRUNC | <Badge type="info" text="Not implemented yet" /> | |

View File

@@ -7,5 +7,6 @@ lang: en-US
# ACOT # ACOT
::: warning ::: warning
🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). 🚧 This function is not yet available in IronCalc.
[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions)
::: :::

Some files were not shown because too many files have changed in this diff Show More