A package for computing combinatorial operations in Typst.
See the code for this example
#import "@preview/combo:0.1.0"
#let items = (
emoji.apple,
emoji.banana,
emoji.cherries,
emoji.peach,
emoji.watermelon,
)
#let n = items.len()
Consider the following *set of distinct items*:
$ F = {#items.join(", ")} $
with cardinality $|F|=#n$.
#let k = 3
How many ways can we choose *$k=#k$ different items* from this list?
$ binom(|F|, k) = (|F|!)/(k! dot (|F|-k)!) = binom(#[#n], #k) = #n!/(#k! dot #(n - k)!) = #combo.count-combinations(n, k) $
Let's list all of them:
#for c in combo.get-combinations(items, k) {
[ + ${#c.join(", ")}$ ]
}
Quick Start 📦
To start using Combo in your Typst project, add the following line to your file:
#import "@preview/combo:0.1.0"
You can then access the functions provided by the package calling the combo
namespace.
Combo Tools 🧰
A simple way to think about the functions provided by this package is to consider that they are defined by 3 choices:
- Which action to perform:
count
orget
groups?count
functions return the number of possible groups.get
functions return the actual groups as a list of lists.
- Which type of groups to consider:
combinations
orpermutations
?combinations
are groups where the order of items does not matter. E.g. the group(a, b)
is the same as(b, a)
.permutations
are groups where the order of items does matter. E.g. the group(a, b, c)
is different from(b, a, c)
, from(c, b, a)
, etc.
- Whether repetition of items is allowed:
with-repetition
orno-repetition
?with-repetition
means that the same item can be chosen multiple times in the same group.no-repetition
means that each item can be chosen only once in the same group.
According to this structure, Combo functions are named following this pattern:
combo.
{count
|get
}-{combinations
|permutations
}-{with-repetition
|no-repetition
}(…parameters…)
For example, the following function counts the number of combinations without repetition of 3 items from a list of 5 items (5 choose 3):
#combo.count-combinations-no-repetition(5, 3) // returns 10
Parameters
Combo functions (usually) take the following parameters:
items
, which represents the pool of items to choose from. It can be either anarray
of such items, or aninteger
representing the cardinality.- If
items
is anarray
, the items in the array can be of any type. Such items will be used as-is in the returned groups. - If
items
is aninteger
, it must be a non-negative integern
, and the items will be considered as the integers from0
ton-1
.
- If
k
: the number of items to choose in each group, which must be a non-negativeinteger
. Depending on which type of groups is considered, the parameterk
may have a default value:- For combinations,
k
has no default value (i.e. it must be provided). - For permutations,
k
defaults toauto
, which means thatk
will be set to the cardinality ofitems
(i.e. all items will be used in each group). This is because permutations are usually considered as arrangements of all items.
- For combinations,
Here’s an example of different function calls:
#combo.count-combinations-with-repetition(("a", "b", "c", "d"), 2)
// returns binom(4 + 2 - 1, 2) = 10
#combo.count-permutations-no-repetition(4)
// returns perm(4, 4) = 24
#combo.count-permutations-with-repetition(("x", "y"), k: 3)
// returns 2^3 = 8
// Notice that `k` is specified using a named argument
Return Values
Combo have two types of return values, depending on the type of function:
count
functions, i.e. those that count the number of groups, return a singleinteger
.get
functions, i.e. those that list the actual groups, return anarray
ofarrays
, where each inner array represents a group of items.
#combo.count-combinations-no-repetition(("a", "b", "c", "d"), 2)
// returns 6
#combo.get-combinations-no-repetition(("a", "b", "c", "d"), 2)
// returns an array of arrays:
// (
// ("a", "b"),
// ("a", "c"),
// ("a", "d"),
// ("b", "c"),
// ("b", "d"),
// ("c", "d"),
// )
List of Functions
Here is the complete list of functions provided by Combo:
Name | Description | Return type |
---|---|---|
count-combinations-no-repetition |
Counts the number of combinations (groups where order doesn’t matter) without repetition (no items duplicated). In mathematical terms, it computes the binomial coefficient for n items, choose k by calling the calc.binom(n, k) Typst native function. |
int |
count-combinations-with-repetition |
Counts the number of combinations (groups where order doesn’t matter) with repetition (items can be duplicated). It’s computed using the formula calc.binom(n + k - 1, k) , where n is the cardinality of items and k is the number of items to choose. |
int |
count-combinations |
Wrapper for the two functions above, with an additional parameter repetition that can be either true or false (default). |
int |
count-permutations-no-repetition |
Counts the number of permutations (groups where order matters) without repetition (no items duplicated). This is computed using the formula n! / (n - k)! , where n is the cardinality of items and k is the number of items to choose. If k is not provided, it defaults to n , and the function returns n! . |
int |
count-permutations-with-repetition |
Counts the number of permutations (groups where order matters) with repetition (items can be duplicated). The result is the cardinality of items raised to the power of k , i.e. n^k . If k is not provided, it defaults to n , and the function returns n^n . |
int |
count-permutations |
Wrapper for the two functions above, with an additional parameter repetition that can be either true or false (default). |
int |
get-combinations-no-repetition |
Gets all the possible combinations (groups where order doesn’t matter) without repetition (no items duplicated). | array of arrays |
get-combinations-with-repetition |
Gets all the possible combinations (groups where order doesn’t matter) with repetition (items can be duplicated). | array of arrays |
get-combinations |
Wrapper for the two functions above, with an additional parameter repetition that can be either true or false (default). |
array of arrays |
get-permutations-no-repetition |
Gets all the possible permutations (groups where order matters) without repetition (no items duplicated). | array of arrays |
get-permutations-with-repetition |
Gets all the possible permutations (groups where order matters) with repetition (items can be duplicated). | array of arrays |
get-permutations |
Wrapper for the two functions above, with an additional parameter repetition that can be either true or false (default). |
array of arrays |
cartesian-product |
Gets the Cartesian product of multiple input arrays. This is an auxiliary utility function that may be needed when dealing with combinatorial problems. | array of arrays |
Important Notes ⚠️
- All functions are pure (i.e. they have no side effects) and deterministic (i.e. they always return the same output for the same input).
- All functions return results in lexicographic order. For example, the combinations of
("a", "b", "c")
taken 2 at a time are returned in the order:(("a", "b"), ("a", "c"), ("b", "c"))
.- Lexicographic order is determined by the order of items in the input array: this means that, for example, given the array
("zzz", "aaa")
as input, the string"zzz"
comes before"aaa"
. - This choice prioritizes consistency and predictability over efficiency, based on the assumption that Typst will not be used in performance-critical scenarios.
- Lexicographic order is determined by the order of items in the input array: this means that, for example, given the array
- All items in the input array
items
are assumed distinct. If the input array contains duplicates, they will be treated as different items.- For example, the combinations of
("a", "b", "a")
taken 2 at a time are:(("a", "b"), ("a", "a"), ("a", "b"))
.
- For example, the combinations of
- The functions that get groups may return a very large number of groups, depending on the input parameters. For example, the number of permutations with repetition of 10 items is
10^10 = 10_000_000_000
. Be careful when using such functions with large inputs, as they may lead to performance issues or even crash Typst.
Examples 📚
Counting the number of permutations for a lock
This simple document is rendered programmatically based on the values of total-digits
and digits-to-choose
, which can be changed to see how the number of possible locking codes changes.
See the code for this example
#import "@preview/combo:0.1.0"
#let total-digits = 10
#let digits-to-choose = 4
We have a lock with a keypad that uses *#total-digits digits*. How many different *locking codes of #digits-to-choose digits* can be formed?
We need to count the number of *permutations with repetition*, which is given by the formula:
$ P^*(#total-digits, #digits-to-choose)
= #total-digits^#digits-to-choose
= #combo.count-permutations-with-repetition(total-digits, k: digits-to-choose) $
Creating different tests for a class of students
In this example, we create a simple three-question quiz by selecting unique combinations of answers from a pool of possible answers.
See the code for this example
#import "@preview/combo:0.1.0"
// Let's assume you have a dataset of questions and answers
// The objective is to create a quiz with a subset of these questions
#let questions = (
(
question: "What is the capital of Italy?",
answers: ("Rome", "Paris", "Venice", "Florence"),
correct: "Rome",
),
(
question: "What is the largest planet in our solar system?",
answers: ("Earth", "Betelgeuse", "Jupiter", "Saturn"),
correct: "Jupiter",
),
(
question: "Who wrote 'The Tempest'?",
answers: ("Ursula K. Le Guin", "Italo Calvino", "W. Shakespeare", "Douglas Adams"),
correct: "W. Shakespeare",
),
(
question: "How many roads must a man walk down?",
answers: ("42", "7", "3", "5"),
correct: "42",
),
(
question: "How many different ways can you choose 3 items from a set of 5 distinct items?",
answers: ("3", "10", "15", "30"),
correct: "10",
),
)
// It's easy to create differentiated versions of the same quiz by selecting different combinations of questions.
#for quiz-questions in combo.get-combinations(questions, 3) [
// Here we have our 3-question test, we can build a template around it.
#block(breakable: false, stroke: 1pt + gray, inset: 10pt, radius: 5pt)[
= Quiz
Answer the following questions:
#for q in quiz-questions [
+ *#q.question*
#stack(dir: ltr,
..q.answers.map(a => box(width: 25%)[
▢ #a
])
)
#v(1em)
]
]
]
Placing people around a table
This example shows how to use Combo to solve a classic combinatorial problem: how many ways can $n$ people be seated around a table?
(The image shows only some of the possible arrangements; the original document continues for other pages.)
See the code for this example
#import "@preview/combo:0.1.0"
#import "@preview/cetz:0.4.2"
#let people = (
"Alice",
"Bob",
"Charlie",
"David",
"Eve",
"Frank",
)
#let n = people.len()
We have a round table and want to seat #n people around it. *How many different seating arrangements are possible?*
At first, we might think that the answer is simply the number of *permutations* of $#n$ people, which is $#n! = #combo.count-permutations(n, repetition: false)$.
However, since the table is round, we can rotate the seating arrangements without changing them. Thus, we need to divide by $#n$ to account for these rotations:
$ n! / n = (n - 1)! = #n!/#n = #(combo.count-permutations(n, repetition: false) / n) $
If we want to list all unique seating arrangements, we can *generate all permutations* and then *filter out those that are equivalent under rotation*. One way to do this is to select only the arrangements where a specific person (e.g. #people.at(0)) is in a fixed position (e.g., the first position).
However, there's a more efficient way to achieve this! We can still fix one person (e.g. #people.at(0)) in the first position and then generate permutations for the remaining $n - 1 =#(n - 1)$ people. This way, we directly get *all unique seating arrangements without needing to filter*.
#let placements = combo.get-permutations-no-repetition(people.slice(1)).map(p => (people.at(0),) + p)
// Function to visualize a seating arrangement
#let show-table(seating) = {
cetz.canvas({
import cetz.draw: *
rect((-55pt, -35pt), (55pt, 38pt), fill: olive, stroke: none)
// Draw table
let r = 25pt
circle((0, 0), radius: r, fill: rgb("#d6b18b"), stroke: rgb("#bfa080") + 3pt)
// Draw people
let angle = 360deg / seating.len()
scale(x: 1.4)
for person in seating {
content(
(0, r),
box(inset: 2pt, radius: 3pt, fill: white.transparentize(10%))[#person],
)
rotate(angle)
}
})
}
#grid(
..placements.map(show-table),
columns: 4,
gutter: 5pt,
)
Contributing 🧩
Found a bug, have an idea, or want to contribute? Feel free to open an issue or pull request on the GitHub repository.
Made something cool with Combo? Let me know — I’d love to feature your work!
Credits
This package is created by Michele Dusi and is licensed under the GNU General Public License v3.0.
Don’t forget to read the Combo Manual for the complete documentation!