Skip to content

Commit e11bd2a

Browse files
authored
Add factorial, permutation and binomial to calculation functions (typst#639)
1 parent 89cf405 commit e11bd2a

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

library/src/compute/calc.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Calculations and processing of numeric values.
22
3+
use std::cmp;
34
use std::cmp::Ordering;
45
use std::ops::Rem;
56

@@ -23,6 +24,9 @@ pub fn module() -> Module {
2324
scope.define("cosh", cosh);
2425
scope.define("tanh", tanh);
2526
scope.define("log", log);
27+
scope.define("fact", fact);
28+
scope.define("perm", perm);
29+
scope.define("binom", binom);
2630
scope.define("floor", floor);
2731
scope.define("ceil", ceil);
2832
scope.define("round", round);
@@ -404,6 +408,128 @@ pub fn log(
404408
Value::Float(result)
405409
}
406410

411+
/// Calculate the factorial of a number.
412+
///
413+
/// ## Example
414+
/// ```example
415+
/// #calc.fact(5)
416+
/// ```
417+
///
418+
/// Display: Factorial
419+
/// Category: calculate
420+
/// Returns: integer
421+
#[func]
422+
pub fn fact(
423+
/// The number whose factorial to calculate. Must be positive.
424+
number: Spanned<u64>,
425+
) -> Value {
426+
let result = factorial_range(1, number.v).and_then(|r| i64::try_from(r).ok());
427+
428+
match result {
429+
None => bail!(number.span, "the factorial result is too large"),
430+
Some(s) => Value::Int(s),
431+
}
432+
}
433+
434+
/// Calculates the product of a range of numbers. Used to calculate permutations.
435+
/// Returns None if the result is larger than `u64::MAX`
436+
fn factorial_range(start: u64, end: u64) -> Option<u64> {
437+
// By convention
438+
if end + 1 < start {
439+
return Some(0);
440+
}
441+
442+
let mut count: u64 = 1;
443+
let real_start: u64 = cmp::max(1, start);
444+
445+
for i in real_start..=end {
446+
count = count.checked_mul(i)?;
447+
}
448+
Some(count)
449+
}
450+
451+
/// Calculate a permutation.
452+
///
453+
/// ## Example
454+
/// ```example
455+
/// #calc.perm(10,5)
456+
/// ```
457+
///
458+
/// Display: Permutation
459+
/// Category: calculate
460+
/// Returns: integer
461+
#[func]
462+
pub fn perm(
463+
/// The base number. Must be positive.
464+
base: Spanned<u64>,
465+
/// The number of permutations. Must be positive.
466+
numbers: Spanned<u64>,
467+
) -> Value {
468+
let base_parsed = base.v;
469+
let numbers_parsed = numbers.v;
470+
471+
let result = if base_parsed + 1 > numbers_parsed {
472+
factorial_range(base_parsed - numbers_parsed + 1, base_parsed)
473+
.and_then(|value| i64::try_from(value).ok())
474+
} else {
475+
// By convention
476+
Some(0)
477+
};
478+
479+
match result {
480+
None => bail!(base.span, "the permutation result is too large"),
481+
Some(s) => Value::Int(s),
482+
}
483+
}
484+
485+
/// Calculate a binomial coefficient.
486+
///
487+
/// ## Example
488+
/// ```example
489+
/// #calc.binom(10,5)
490+
/// ```
491+
///
492+
/// Display: Permutation
493+
/// Category: calculate
494+
/// Returns: integer
495+
#[func]
496+
pub fn binom(
497+
/// The upper coefficient. Must be positive
498+
n: Spanned<u64>,
499+
/// The lower coefficient. Must be positive.
500+
k: Spanned<u64>,
501+
) -> Value {
502+
let result = binomial(n.v, k.v).and_then(|raw| i64::try_from(raw).ok());
503+
504+
match result {
505+
None => bail!(n.span, "the binomial result is too large"),
506+
Some(r) => Value::Int(r),
507+
}
508+
}
509+
510+
/// Calculates a binomial coefficient, with `n` the upper coefficient and `k` the lower coefficient.
511+
/// Returns `None` if the result is larger than `u64::MAX`
512+
fn binomial(n: u64, k: u64) -> Option<u64> {
513+
if k > n {
514+
return Some(0);
515+
}
516+
517+
// By symmetry
518+
let real_k = cmp::min(n - k, k);
519+
520+
if real_k == 0 {
521+
return Some(1);
522+
}
523+
524+
let mut result: u64 = 1;
525+
526+
for i in 0..real_k {
527+
result = result.checked_mul(n - i).and_then(|r| r.checked_div(i + 1))?;
528+
}
529+
530+
Some(result)
531+
}
532+
407533
/// Round a number down to the nearest integer.
408534
///
409535
/// If the number is already an integer, it is returned unchanged.

src/eval/cast.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ cast_to_value! {
114114
v: usize => Value::Int(v as i64)
115115
}
116116

117+
cast_from_value! {
118+
u64,
119+
int: i64 => int.try_into().map_err(|_| {
120+
if int < 0 {
121+
"number must be at least zero"
122+
} else {
123+
"number too large"
124+
}
125+
})?,
126+
}
127+
128+
cast_to_value! {
129+
v: u64 => Value::Int(v as i64)
130+
}
131+
117132
cast_from_value! {
118133
NonZeroUsize,
119134
int: i64 => int

tests/typ/compute/calc.typ

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,34 @@
114114
// Error: 11-13 the result is not a real number
115115
#calc.log(10, base: -1)
116116

117+
---
118+
// Test the `fact` function.
119+
#test(calc.fact(0), 1)
120+
#test(calc.fact(5), 120)
121+
122+
---
123+
// Error: 12-14 the factorial result is too large
124+
#calc.fact(21)
125+
126+
---
127+
// Test the `perm` function.
128+
#test(calc.perm(0, 0), 1)
129+
#test(calc.perm(5, 3), 60)
130+
#test(calc.perm(5, 5), 120)
131+
#test(calc.perm(5, 6), 0)
132+
133+
---
134+
// Error: 12-14 the permutation result is too large
135+
#calc.perm(21, 21)
136+
137+
---
138+
// Test the `binom` function.
139+
#test(calc.binom(0, 0), 1)
140+
#test(calc.binom(5, 3), 10)
141+
#test(calc.binom(5, 5), 1)
142+
#test(calc.binom(5, 6), 0)
143+
#test(calc.binom(6, 2), 15)
144+
117145
---
118146
// Error: 10-12 expected at least one value
119147
#calc.min()

0 commit comments

Comments
 (0)