summaryrefslogtreecommitdiff
path: root/library/src
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2023-01-30 21:04:34 +0100
committerMartin Haug <mhaug@live.de>2023-01-30 21:04:34 +0100
commit0287b98ef31172c6da4d5a4c76d8d88d1d5c9049 (patch)
tree000bd243993a5212ce52c08374cf20cd0f61bcf9 /library/src
parent1ea0a933254d866e00acb9034bba39a5f4790682 (diff)
Add calc module
Diffstat (limited to 'library/src')
-rw-r--r--library/src/basics/table.rs2
-rw-r--r--library/src/compute/calc.rs600
-rw-r--r--library/src/lib.rs14
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);