diff options
| author | Martin Haug <mhaug@live.de> | 2023-01-30 21:04:34 +0100 |
|---|---|---|
| committer | Martin Haug <mhaug@live.de> | 2023-01-30 21:04:34 +0100 |
| commit | 0287b98ef31172c6da4d5a4c76d8d88d1d5c9049 (patch) | |
| tree | 000bd243993a5212ce52c08374cf20cd0f61bcf9 /library/src | |
| parent | 1ea0a933254d866e00acb9034bba39a5f4790682 (diff) | |
Add calc module
Diffstat (limited to 'library/src')
| -rw-r--r-- | library/src/basics/table.rs | 2 | ||||
| -rw-r--r-- | library/src/compute/calc.rs | 600 | ||||
| -rw-r--r-- | library/src/lib.rs | 14 |
3 files changed, 586 insertions, 30 deletions
diff --git a/library/src/basics/table.rs b/library/src/basics/table.rs index 20881830..6ee2b0b2 100644 --- a/library/src/basics/table.rs +++ b/library/src/basics/table.rs @@ -79,7 +79,7 @@ impl TableNode { /// # Example /// ``` /// #table( - /// fill: (col, _) => if odd(col) { luma(240) } else { white }, + /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white }, /// align: (col, row) => /// if row == 0 { center } /// else if col == 0 { left } diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs index 640ec3c5..c65d32a0 100644 --- a/library/src/compute/calc.rs +++ b/library/src/compute/calc.rs @@ -1,15 +1,45 @@ use std::cmp::Ordering; +use typst::model::{Module, Scope}; + use crate::prelude::*; +/// A module with computational functions. +pub fn calc() -> Module { + let mut scope = Scope::new(); + scope.def_func::<AbsFunc>("abs"); + scope.def_func::<PowFunc>("pow"); + scope.def_func::<SqrtFunc>("sqrt"); + scope.def_func::<SinFunc>("sin"); + scope.def_func::<CosFunc>("cos"); + scope.def_func::<TanFunc>("tan"); + scope.def_func::<AsinFunc>("asin"); + scope.def_func::<AcosFunc>("acos"); + scope.def_func::<AtanFunc>("atan"); + scope.def_func::<SinhFunc>("sinh"); + scope.def_func::<CoshFunc>("cosh"); + scope.def_func::<TanhFunc>("tanh"); + scope.def_func::<LogFunc>("log"); + scope.def_func::<FloorFunc>("floor"); + scope.def_func::<CeilFunc>("ceil"); + scope.def_func::<RoundFunc>("round"); + scope.def_func::<ClampFunc>("clamp"); + scope.def_func::<MinFunc>("min"); + scope.def_func::<MaxFunc>("max"); + scope.def_func::<EvenFunc>("even"); + scope.def_func::<OddFunc>("odd"); + scope.def_func::<ModFunc>("mod"); + Module::new("calc").with_scope(scope) +} + /// # Absolute /// The absolute value of a numeric value. /// /// ## Example /// ``` -/// #abs(-5) \ -/// #abs(5pt - 2cm) \ -/// #abs(2fr) +/// #calc.abs(-5) \ +/// #calc.abs(5pt - 2cm) \ +/// #calc.abs(2fr) /// ``` /// /// ## Parameters @@ -37,13 +67,482 @@ castable! { v: Fr => Self(Value::Fraction(v.abs())), } +/// # Power +/// Raise a value to some exponent. +/// +/// ## Example +/// ``` +/// #calc.pow(2, 3) +/// ``` +/// +/// ## Parameters +/// - base: Num (positional, required) +/// The base of the power. +/// - exponent: Num (positional, required) +/// The exponent of the power. Must be non-negative. +/// +/// ## Category +/// calculate +#[func] +pub fn pow(args: &mut Args) -> SourceResult<Value> { + let base = args.expect::<Num>("base")?; + let exponent = args + .expect::<Spanned<Num>>("exponent") + .and_then(|n| match n.v { + Num::Int(i) if i > u32::MAX as i64 => bail!(n.span, "exponent too large"), + Num::Int(i) if i >= 0 => Ok(n), + Num::Float(f) if f >= 0.0 => Ok(n), + _ => bail!(n.span, "exponent must be non-negative"), + })? + .v; + + Ok(base.apply2(exponent, |a, b| a.pow(b as u32), |a, b| a.powf(b))) +} + +/// # Square Root +/// The square root of a number. +/// +/// ## Example +/// ``` +/// #calc.sqrt(16) \ +/// #calc.sqrt(2.5) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number whose square root to calculate. Must be non-negative. +/// +/// ## Category +/// calculate +#[func] +pub fn sqrt(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Spanned<Num>>("value")?; + if value.v.is_negative() { + bail!(value.span, "cannot take square root of negative number"); + } + Ok(Value::Float(value.v.float().sqrt())) +} + +/// # Sine +/// Calculate the sine of an angle. When called with an integer or a number, +/// they will be interpreted as radians. +/// +/// ## Example +/// ``` +/// #assert(calc.sin(90deg) == calc.sin(-270deg)) +/// #calc.sin(1.5) \ +/// #calc.sin(90deg) +/// ``` +/// +/// ## Parameters +/// - angle: AngleLike (positional, required) +/// The angle whose sine to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn sin(args: &mut Args) -> SourceResult<Value> { + let arg = args.expect::<AngleLike>("angle")?; + + Ok(Value::Float(match arg { + AngleLike::Angle(a) => a.sin(), + AngleLike::Int(n) => (n as f64).sin(), + AngleLike::Float(n) => n.sin(), + })) +} + +/// # Cosine +/// Calculate the cosine of an angle. When called with an integer or a number, +/// they will be interpreted as radians. +/// +/// ## Example +/// ``` +/// #calc.cos(90deg) +/// #calc.cos(1.5) \ +/// #calc.cos(90deg) +/// ``` +/// +/// ## Parameters +/// - angle: AngleLike (positional, required) +/// The angle whose cosine to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn cos(args: &mut Args) -> SourceResult<Value> { + let arg = args.expect::<AngleLike>("angle")?; + + Ok(Value::Float(match arg { + AngleLike::Angle(a) => a.cos(), + AngleLike::Int(n) => (n as f64).cos(), + AngleLike::Float(n) => n.cos(), + })) +} + +/// # Tangent +/// Calculate the tangent of an angle. When called with an integer or a number, +/// they will be interpreted as radians. +/// +/// ## Example +/// ``` +/// #calc.tan(1.5) \ +/// #calc.tan(90deg) +/// ``` +/// +/// ## Parameters +/// - angle: AngleLike (positional, required) +/// The angle whose tangent to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn tan(args: &mut Args) -> SourceResult<Value> { + let arg = args.expect::<AngleLike>("angle")?; + + Ok(Value::Float(match arg { + AngleLike::Angle(a) => a.tan(), + AngleLike::Int(n) => (n as f64).tan(), + AngleLike::Float(n) => n.tan(), + })) +} + +/// # Arcsine +/// Calculate the arcsine of a number. +/// +/// ## Example +/// ``` +/// #calc.asin(0) \ +/// #calc.asin(1) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number whose arcsine to calculate. Must be between -1 and 1. +/// +/// ## Category +/// calculate +#[func] +pub fn asin(args: &mut Args) -> SourceResult<Value> { + let Spanned { v, span } = args.expect::<Spanned<Num>>("value")?; + let val = v.float(); + if val < -1.0 || val > 1.0 { + bail!(span, "arcsin must be between -1 and 1"); + } + + Ok(Value::Angle(Angle::rad(val.asin()))) +} + +/// # Arccosine +/// Calculate the arccosine of a number. +/// +/// ## Example +/// ``` +/// #calc.acos(0) \ +/// #calc.acos(1) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number whose arccosine to calculate. Must be between -1 and 1. +/// +/// ## Category +/// calculate +#[func] +pub fn acos(args: &mut Args) -> SourceResult<Value> { + let Spanned { v, span } = args.expect::<Spanned<Num>>("value")?; + let val = v.float(); + if val < -1.0 || val > 1.0 { + bail!(span, "arccos must be between -1 and 1"); + } + + Ok(Value::Angle(Angle::rad(val.acos()))) +} + +/// # Arctangent +/// Calculate the arctangent of a number. +/// +/// ## Example +/// ``` +/// #calc.atan(0) \ +/// #calc.atan(1) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number whose arctangent to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn atan(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Num>("value")?; + + Ok(Value::Angle(Angle::rad(value.float().atan()))) +} + +/// # Hyperbolic sine +/// Calculate the hyperbolic sine of an angle. When called with an integer or +/// a number, they will be interpreted as radians. +/// +/// ## Example +/// ``` +/// #calc.sinh(0) \ +/// #calc.sinh(45deg) +/// ``` +/// +/// ## Parameters +/// - angle: AngleLike (positional, required) +/// The angle whose hyperbolic sine to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn sinh(args: &mut Args) -> SourceResult<Value> { + let arg = args.expect::<AngleLike>("angle")?; + + Ok(Value::Float(match arg { + AngleLike::Angle(a) => a.to_rad(), + AngleLike::Int(n) => (n as f64).sinh(), + AngleLike::Float(n) => n.sinh(), + })) +} + +/// # Hyperbolic cosine +/// Calculate the hyperbolic cosine of an angle. When called with an integer or +/// a number, they will be interpreted as radians. +/// +/// ## Example +/// ``` +/// #calc.cosh(0) \ +/// #calc.cosh(45deg) +/// ``` +/// +/// ## Parameters +/// - angle: AngleLike (positional, required) +/// The angle whose hyperbolic cosine to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn cosh(args: &mut Args) -> SourceResult<Value> { + let arg = args.expect::<AngleLike>("angle")?; + + Ok(Value::Float(match arg { + AngleLike::Angle(a) => a.to_rad(), + AngleLike::Int(n) => (n as f64).cosh(), + AngleLike::Float(n) => n.cosh(), + })) +} + +/// # Hyperbolic tangent +/// Calculate the hyperbolic tangent of an angle. When called with an integer or +/// a number, they will be interpreted as radians. +/// +/// ## Example +/// ``` +/// #calc.tanh(0) \ +/// #calc.tanh(45deg) +/// ``` +/// +/// ## Parameters +/// - angle: AngleLike (positional, required) +/// The angle whose hyperbolic tangent to calculate. +/// +/// ## Category +/// calculate +#[func] +pub fn tanh(args: &mut Args) -> SourceResult<Value> { + let arg = args.expect::<AngleLike>("angle")?; + + Ok(Value::Float(match arg { + AngleLike::Angle(a) => a.to_rad(), + AngleLike::Int(n) => (n as f64).tanh(), + AngleLike::Float(n) => n.tanh(), + })) +} + +/// # Logarithm +/// Calculate the logarithm of a number. +/// If the base is not specified, the logarithm is calculated in base 10. +/// +/// ## Example +/// ``` +/// #calc.log(100) \ +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number whose logarithm to calculate. +/// - base: Num (named) +/// The base of the logarithm. +/// +/// ## Category +/// calculate +#[func] +pub fn log(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Num>("value")?; + let base = args.named::<Num>("base")?.unwrap_or_else(|| Num::Int(10)); + + Ok(value.apply2(base, |a, b| a.ilog(b) as i64, |a, b| a.log(b))) +} + +/// # Round down +/// Round a number down to the nearest integer. +/// If the number is already an integer, it is returned unchanged. +/// +/// ## Example +/// ``` +/// #assert(calc.floor(3.14) == 3) +/// #assert(calc.floor(3) == 3) +/// #calc.floor(500.1) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number to round down. +/// +/// ## Category +/// calculate +#[func] +pub fn floor(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Num>("value")?; + + Ok(match value { + Num::Int(n) => Value::Int(n), + Num::Float(n) => Value::Int(n.floor() as i64), + }) +} + +/// # Round up +/// Round a number up to the nearest integer. +/// If the number is already an integer, it is returned unchanged. +/// +/// ## Example +/// ``` +/// #assert(calc.ceil(3.14) == 4) +/// #assert(calc.ceil(3) == 3) +/// #calc.ceil(500.1) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number to round up. +/// +/// ## Category +/// calculate +#[func] +pub fn ceil(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Num>("value")?; + + Ok(match value { + Num::Int(n) => Value::Int(n), + Num::Float(n) => Value::Int(n.ceil() as i64), + }) +} + +/// # Round +/// Round a number to the nearest integer. +/// Optionally, a number of decimal places can be specified. +/// +/// ## Example +/// ``` +/// #assert(calc.round(3.14) == 3) +/// #assert(calc.round(3.5) == 4) +/// #calc.round(3.1415, digits: 2) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number to round. +/// - digits: i64 (named) +/// +/// ## Category +/// calculate +#[func] +pub fn round(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Num>("value")?; + let digits = args.named::<Spanned<i64>>("digits").and_then(|n| match n { + Some(Spanned { v, span }) if v < 0 => { + bail!(span, "digits must be non-negative") + } + Some(Spanned { v, span }) if v > i32::MAX as i64 => { + bail!(span, "digits must be less than {}", i32::MAX) + } + Some(Spanned { v, .. }) => Ok(v as i32), + None => Ok(0), + })?; + + Ok(match value { + Num::Int(n) if digits == 0 => Value::Int(n), + _ => { + let n = value.float(); + let factor = 10.0_f64.powi(digits) as f64; + Value::Float((n * factor).round() / factor) + } + }) +} + +/// # Clamp +/// Clamp a number between a minimum and maximum value. +/// +/// ## Example +/// ``` +/// #assert(calc.clamp(5, 0, 10) == 5) +/// #assert(calc.clamp(5, 6, 10) == 6) +/// #calc.clamp(5, 0, 4) +/// ``` +/// +/// ## Parameters +/// - value: Num (positional, required) +/// The number to clamp. +/// - min: Num (positional, required) +/// The inclusive minimum value. +/// - max: Num (positional, required) +/// The inclusive maximum value. +/// +/// ## Category +/// calculate +#[func] +pub fn clamp(args: &mut Args) -> SourceResult<Value> { + let value = args.expect::<Num>("value")?; + let min = args.expect::<Num>("min")?; + let max = args.expect::<Spanned<Num>>("max")?; + + if max.v.float() < min.float() { + bail!(max.span, "max must be greater than or equal to min") + } + + Ok(value.apply3( + min, + max.v, + |v, min, max| { + if v < min { + min + } else if v > max { + max + } else { + v + } + }, + |v, min, max| { + if v < min { + min + } else if v > max { + max + } else { + v + } + }, + )) +} + /// # Minimum /// The minimum of a sequence of values. /// /// ## Example /// ``` -/// #min(1, -3, -5, 20, 3, 6) \ -/// #min("typst", "in", "beta") +/// #calc.min(1, -3, -5, 20, 3, 6) \ +/// #calc.min("typst", "in", "beta") /// ``` /// /// ## Parameters @@ -65,8 +564,8 @@ pub fn min(args: &mut Args) -> SourceResult<Value> { /// /// ## Example /// ``` -/// #max(1, -3, -5, 20, 3, 6) \ -/// #max("typst", "in", "beta") +/// #calc.max(1, -3, -5, 20, 3, 6) \ +/// #calc.max("typst", "in", "beta") /// ``` /// /// ## Parameters @@ -109,9 +608,9 @@ fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> { /// /// ## Example /// ``` -/// #even(4) \ -/// #even(5) \ -/// #range(10).filter(even) +/// #calc.even(4) \ +/// #calc.even(5) \ +/// #range(10).filter(calc.even) /// ``` /// /// ## Parameters @@ -132,9 +631,9 @@ pub fn even(args: &mut Args) -> SourceResult<Value> { /// /// ## Example /// ``` -/// #odd(4) \ -/// #odd(5) \ -/// #range(10).filter(odd) +/// #calc.odd(4) \ +/// #calc.odd(5) \ +/// #range(10).filter(calc.odd) /// ``` /// /// @@ -156,15 +655,15 @@ pub fn odd(args: &mut Args) -> SourceResult<Value> { /// /// ## Example /// ``` -/// #mod(20, 6) \ -/// #mod(1.75, 0.5) +/// #calc.mod(20, 6) \ +/// #calc.mod(1.75, 0.5) /// ``` /// /// ## Parameters -/// - dividend: ToMod (positional, required) +/// - dividend: Num (positional, required) /// The dividend of the modulus. /// -/// - divisor: ToMod (positional, required) +/// - divisor: Num (positional, required) /// The divisor of the modulus. /// /// - returns: integer or float @@ -200,10 +699,69 @@ pub fn mod_(args: &mut Args) -> SourceResult<Value> { } /// A value which can be passed to the `mod` function. -struct ToMod; +#[derive(Debug, Copy, Clone)] +enum Num { + Int(i64), + Float(f64), +} + +impl Num { + fn apply2( + self, + other: Self, + int: impl FnOnce(i64, i64) -> i64, + float: impl FnOnce(f64, f64) -> f64, + ) -> Value { + match (self, other) { + (Self::Int(a), Self::Int(b)) => Value::Int(int(a, b)), + (a, b) => Value::Float(float(a.float(), b.float())), + } + } + + fn apply3( + self, + other: Self, + third: Self, + int: impl FnOnce(i64, i64, i64) -> i64, + float: impl FnOnce(f64, f64, f64) -> f64, + ) -> Value { + match (self, other, third) { + (Self::Int(a), Self::Int(b), Self::Int(c)) => Value::Int(int(a, b, c)), + (a, b, c) => Value::Float(float(a.float(), b.float(), c.float())), + } + } + + fn float(self) -> f64 { + match self { + Self::Int(v) => v as f64, + Self::Float(v) => v, + } + } + + fn is_negative(self) -> bool { + match self { + Self::Int(v) => v < 0, + Self::Float(v) => v < 0.0, + } + } +} + +castable! { + Num, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), +} + +/// A value that can be passed to a trigonometric function. +enum AngleLike { + Int(i64), + Float(f64), + Angle(Angle), +} castable! { - ToMod, - _: i64 => Self, - _: f64 => Self, + AngleLike, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Angle => Self::Angle(v), } diff --git a/library/src/lib.rs b/library/src/lib.rs index 08ff171a..a2f52549 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -19,12 +19,13 @@ use self::layout::LayoutRoot; pub fn build() -> Library { let sym = text::sym(); let math = math::module(&sym); - let global = global(sym, math.clone()); + let calc = compute::calc(); + let global = global(sym, math.clone(), calc); Library { global, math, styles: styles(), items: items() } } /// Construct the module with global definitions. -fn global(sym: Module, math: Module) -> Module { +fn global(sym: Module, math: Module, calc: Module) -> Module { let mut global = Scope::deduplicating(); // Basics. @@ -106,12 +107,6 @@ fn global(sym: Module, math: Module) -> Module { global.def_func::<compute::LabelFunc>("label"); global.def_func::<compute::RegexFunc>("regex"); global.def_func::<compute::RangeFunc>("range"); - global.def_func::<compute::AbsFunc>("abs"); - global.def_func::<compute::MinFunc>("min"); - global.def_func::<compute::MaxFunc>("max"); - global.def_func::<compute::EvenFunc>("even"); - global.def_func::<compute::OddFunc>("odd"); - global.def_func::<compute::ModFunc>("mod"); global.def_func::<compute::ReadFunc>("read"); global.def_func::<compute::CsvFunc>("csv"); global.def_func::<compute::JsonFunc>("json"); @@ -119,6 +114,9 @@ fn global(sym: Module, math: Module) -> Module { global.def_func::<compute::LoremFunc>("lorem"); global.def_func::<compute::NumberingFunc>("numbering"); + // Calc. + global.define("calc", calc); + // Colors. global.define("black", Color::BLACK); global.define("gray", Color::GRAY); |
