Merge pull request #280 from vangel76/feat/percentof-euroconvert

Functions: PERCENTOF and EUROCONVERT (#279)

# Conflicts:
#	README.md
#	ROADMAP.md
#	crates/functions/src/financial.rs
#	docs/function-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
This commit is contained in:
Brandon Thomas
2026-10-11 11:44:46 -04:00
8 changed files with 140 additions and 4 deletions
+94 -1
View File
@@ -6,7 +6,7 @@
use gridcraft_core::date::{datetime_from_serial, days_in_month, serial_from_ymd};
use gridcraft_core::{CellError, DateSystem, Value};
use crate::util::{R, S, array_numbers, as_array, flatten, has, num, num_val, numbers, opt_bool, opt_num};
use crate::util::{R, S, array_numbers, as_array, flatten, has, num, num_val, numbers, opt_bool, opt_num, round_half_away, text, to_sig_digits};
use crate::{Arg, Ctx, FnSpec, VAR};
// ---------------------------------------------------------------------------------------------
@@ -1305,6 +1305,64 @@ fn oddlyield(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val((red + dc * coupon - paid) / paid * f / dsc)
}
// ---------------------------------------------------------------------------------------------
// Euro conversion
// ---------------------------------------------------------------------------------------------
/// The currencies EUROCONVERT documents: ISO code, the irrevocable conversion rate in units per
/// euro (Council Regulation (EC) No 2866/98 as amended) and the calculation precision in decimals.
const EURO_RATES: &[(&str, f64, i32)] = &[
("BEF", 40.3399, 0),
("LUF", 40.3399, 0),
("DEM", 1.95583, 2),
("ESP", 166.386, 0),
("FRF", 6.55957, 2),
("IEP", 0.787564, 2),
("ITL", 1936.27, 0),
("NLG", 2.20371, 2),
("ATS", 13.7603, 2),
("PTE", 200.482, 0),
("FIM", 5.94573, 2),
("GRD", 340.75, 0),
("SIT", 239.64, 2),
("EUR", 1.0, 2),
];
fn euro_currency(a: &[Arg], i: usize) -> R<(&'static str, f64, i32)> {
let code = text(a, i)?;
let code = code.trim();
EURO_RATES.iter().copied().find(|(c, _, _)| c.eq_ignore_ascii_case(code)).ok_or(CellError::Value)
}
/// EUROCONVERT(number, source, target, [full_precision], [triangulation_precision]).
fn euroconvert(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = num(a, 0)?;
let (src, src_rate, _) = euro_currency(a, 1)?;
let (tgt, tgt_rate, tgt_prec) = euro_currency(a, 2)?;
let full = opt_bool(a, 3, false)?;
let triangulation = if has(a, 4) {
let t = num(a, 4)?.trunc();
if t < 3.0 {
return Err(CellError::Value);
}
// Beyond 17 significant digits rounding changes nothing.
Some(t.min(17.0) as usize)
} else {
None
};
if src == tgt {
return num_val(x);
}
let mut euros = x / src_rate;
if src != "EUR"
&& let Some(t) = triangulation
{
euros = to_sig_digits(euros, t);
}
let out = euros * tgt_rate;
num_val(if full { out } else { round_half_away(out, tgt_prec) })
}
pub(crate) fn specs() -> Vec<FnSpec> {
vec![
f!(
@@ -1686,6 +1744,16 @@ pub(crate) fn specs() -> Vec<FnSpec> {
"Yield of a security with an odd last period.",
oddlyield
),
f!(
"EUROCONVERT",
3,
5,
AddIn,
S,
"EUROCONVERT(number, source, target, [full_precision], [triangulation_precision])",
"Converts between the euro and the former euro member currencies at the fixed rates.",
euroconvert
),
]
}
@@ -1916,6 +1984,31 @@ mod tests {
is_err(ev("ODDFPRICE", vec![n(-5.0), n(1e20), n(0.0), n(1e10), n(0.05), n(0.05), n(100.0), n(2.0)]), CellError::Num);
}
#[test]
fn euroconvert() {
// The examples on Microsoft's EUROCONVERT page.
let ec = |x: f64, s: &str, tg: &str, rest: Vec<Arg>| {
let mut a = vec![n(x), t(s), t(tg)];
a.extend(rest);
ev("EUROCONVERT", a)
};
close(ec(1.2, "DEM", "EUR", vec![]), 0.61);
close(ec(1.0, "FRF", "EUR", vec![b(true), n(3.0)]), 0.152);
close(ec(1.0, "FRF", "EUR", vec![b(false), n(3.0)]), 0.15);
close(ec(1.0, "FRF", "DEM", vec![b(true), n(3.0)]), 0.29728616);
// Euro to a member currency rounds to the target's calculation precision (lira: 0).
close(ec(10.0, "eur", "ITL", vec![]), 19363.0);
close(ec(100.0, "EUR", "IEP", vec![b(true)]), 78.7564);
// Same currency returns the number untouched.
close(ec(1.23456, "DEM", "DEM", vec![]), 1.23456);
// Unknown codes and a triangulation precision below 3 are #VALUE!.
is_err(ec(1.0, "USD", "EUR", vec![]), CellError::Value);
is_err(ec(1.0, "FRF", "DEM", vec![b(true), n(2.0)]), CellError::Value);
// Hostile magnitudes give a number or #NUM!, never a panic.
is_err(ec(f64::MAX, "EUR", "ITL", vec![]), CellError::Num);
close(ec(1.0, "FRF", "DEM", vec![b(true), n(1e300)]), 1.95583 / 6.55957);
}
#[test]
fn lifting() {
let v = ev("SLN", vec![av(row(&[100.0, 200.0])), n(0.0), n(10.0)]);
+2
View File
@@ -122,6 +122,8 @@ pub enum Category {
Compatibility,
Web,
Cube,
/// Excel's "Add-in and Automation" category (EUROCONVERT).
AddIn,
}
pub struct FnSpec {
+33
View File
@@ -86,6 +86,17 @@ fn sum(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(numbers(a)?.iter().sum())
}
/// PERCENTOF(data_subset, data_all) = SUM(data_subset) / SUM(data_all).
fn percentof(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let part = a.get(..1).ok_or(CellError::Value)?;
let all = a.get(1..2).ok_or(CellError::Value)?;
let (part, all): (f64, f64) = (numbers(part)?.iter().sum(), numbers(all)?.iter().sum());
if all == 0.0 {
return Err(CellError::Div0);
}
num_val(part / all)
}
fn sumsq(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(numbers(a)?.iter().map(|x| x * x).sum())
}
@@ -1050,6 +1061,16 @@ fn averageif(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
pub(crate) fn specs() -> Vec<FnSpec> {
vec![
f!("SUM", 1, VAR, MathTrig, A, "SUM(number1, [number2], ...)", "Adds all the numbers in the arguments.", sum),
f!(
"PERCENTOF",
2,
2,
MathTrig,
A,
"PERCENTOF(data_subset, data_all)",
"The sum of a subset as a fraction of the sum of all the values.",
percentof
),
f!("SUMSQ", 1, VAR, MathTrig, A, "SUMSQ(number1, [number2], ...)", "Adds the squares of the numbers.", sumsq),
f!(
"SUMPRODUCT",
@@ -1278,6 +1299,18 @@ mod tests {
ev(name, vec![n(x), n(y)])
}
#[test]
fn percentof() {
// SUM(subset) / SUM(all): text, booleans and blanks inside ranges are skipped like SUM.
let all = arr(vec![vec![nv(10.0)], vec![nv(30.0)], vec![tv("x")], vec![Value::Empty], vec![nv(60.0)]]);
let part = arr(vec![vec![nv(10.0)], vec![nv(30.0)]]);
close(ev("PERCENTOF", vec![rf(part), rf(all.clone())]), 0.4);
close(ev("PERCENTOF", vec![n(25.0), n(200.0)]), 0.125);
is_err(ev("PERCENTOF", vec![n(1.0), rf(arr(vec![vec![nv(5.0), nv(-5.0)]]))]), Div0);
is_err(ev("PERCENTOF", vec![rf(arr(vec![vec![Value::Error(NA)]])), rf(all)]), NA);
is_err(ev("PERCENTOF", vec![t("abc"), n(1.0)]), CellError::Value);
}
#[test]
fn sums() {
close(ev("SUM", vec![n(1.0), n(2.0), t("3"), b(true)]), 7.0);