Skip to content

Commit

Permalink
combinations and permutations
Browse files Browse the repository at this point in the history
  • Loading branch information
jmagaram committed Apr 25, 2023
2 parents f7948a2 + 690754d commit a49d8e2
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## Version 1.1.1

- Fix bug in `Seq.takeAtMost` where generator function was called 1 too many times; not lazy enough
- `Seq.combinations`
- `Seq.permutations`

## Version 1.1.0

Expand Down
3 changes: 3 additions & 0 deletions SEQ_CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ If you want to add some functions or improve the package, start by posting an is

- For each new function, add a collection of tests
- Do one stress test to ensure no stack overflows, especially with recursion
- Think about how the behavior changes with dynamic/non-persistent sequences, such as where each item is generated on the fly like Js.Math.random(). For example, `allPairs` chooses to cache the values before generating the pairs.
- Ensure any supplied functions are only called the minimum number of times.
- Try taking 0 items from an infinite sequence as a test of complete laziness.

### Coding style

Expand Down
2 changes: 2 additions & 0 deletions SEQ_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type t<'a> // A lazy sequence of `a`
// Construct
let characters: string => t<string>
let combinations: (t<'a>, int) => t<(int, t<'a>)>
let cons: ('a, t<'a>) => t<'a>
let cycle: t<'a> => t<'a>
let delay:()=>t<'a>=>t<'a>
Expand All @@ -46,6 +47,7 @@ let fromList: list<'a> => t<'a>
let fromOption: option<'a> => t<'a>
let init: (int, int => 'a) => t<'a>
let iterate: ('a, 'a => 'a) => t<'a>
let permutations: (t<'a>, int) => t<(int, t<'a>)>
let range: (int, int) => t<int>
let rangeMap: (int, int, int => 'a) => t<'a>
let repeat: (int, 'a) => t<'a>
Expand Down
58 changes: 58 additions & 0 deletions src/Extras__Seq.res
Original file line number Diff line number Diff line change
Expand Up @@ -720,3 +720,61 @@ let sortBy = (xx, compare) =>
xx->Js.Array2.sortInPlaceWith(compare)->ignore
xx->fromArray
})

/**
`distribute(source, divider)` iterates through each item in `source` and returns
the sequences that result when `divider` is inserted before that item, and also
after the last item. If `source` is empty, returns a sequence consisting only of
`divider`.
```
[1, 2]->distribute(8) // [[8,1,2], [1,8,2], [1,2,8]]
[1]->distribute(8) // [[8,1], [1,8]]
[]->distribute(8) // [[8]]
```
*/
let distribute = (xx, item) => {
let go = (pre, suf) =>
unfold((pre, suf), ((pre, suf)) =>
switch suf->headTail {
| None => None
| Some(x, xx) => {
let yield = pre->endWith(x)->endWith(item)->concat(xx)
let next = (pre->endWith(x), xx)
Some(yield, next)
}
}
)
go(empty, xx)->startWith(cons(item, xx))
}

let (combinations, permutations) = {
let helper = (xx, maxSize, f) => {
if maxSize <= 0 {
ArgumentOfOfRange(
`Size must be 1 or more. You asked for ${maxSize->Belt.Int.toString}.`,
)->raise
}
unfold((empty, xx), ((sum, xx)) =>
switch xx->headTail {
| None => None
| Some(x, xx) => {
let next = {
let xOnly = (1, x->singleton)
let xWithSum = sum->flatMap(((size, xx)) =>
switch size < maxSize {
| true => f(x, xx)->map(xx => (size + 1, xx))
| false => empty
}
)
cons(xOnly, xWithSum)
}
Some(next, (concat(sum, next), xx))
}
}
)->flatten
}
let permutations = (xx, maxSize) => helper(xx, maxSize, (x, xx) => distribute(xx, x))
let combinations = (xx, maxSize) => helper(xx, maxSize, (x, xx) => singleton(cons(x, xx)))
(combinations, permutations)
}
16 changes: 16 additions & 0 deletions src/Extras__Seq.resi
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,19 @@ sequences. The function uses a stable sort (the original order of equal elements
is preserved) based on the provided `compare` function.
*/
let sortBy: (t<'a>, ('a, 'a) => int) => t<'a>

/**
`combinations(source, size)` generates all combinations of items in `source`,
based on their indexed position (not value), from 1 to `size` items in length.
Each combination is returned as a `(size, sequence)` tuple. A combination is a
unique set; the order in which items are listed is immaterial.
*/
let combinations: (t<'a>, int) => t<(int, t<'a>)>

/**
`permutations(source, size)` generates all permutations (ordered arrangements)
of items in `source` based on their indexed position (not value), from 1 to
`size` items in length. Each permutation is returned as a `(size, sequence)`
tuple.
*/
let permutations: (t<'a>, int) => t<(int, t<'a>)>
163 changes: 163 additions & 0 deletions tests/Extras__SeqTests.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module R = Extras__Result
module Ex = Extras
module Option = Belt.Option
module Result = Belt.Result
module String = Js.String2

// =============================
// Utilities to help write tests
Expand All @@ -12,6 +13,7 @@ module Result = Belt.Result
let intToString = Belt.Int.toString

let intCompare = Ex.Cmp.int
let stringCmp = (a: string, b: string) => a < b ? -1 : a > b ? 1 : 0

let concatInts = xs =>
xs->Js.Array2.length == 0 ? "_" : xs->Js.Array2.map(intToString)->Js.Array2.joinWith("")
Expand All @@ -25,6 +27,22 @@ let oneTwoThree = S.range(1, 3)
let fourFiveSix = S.range(4, 6)
let oneToFive = S.range(1, 5)

/**
Constructs an infinite sequence that returns the number of times it has been
invoked. Useful for tracking that various sequences are completely lazy. For
example, if you use this sequence and do a `takeAtMost(3)` then it shouldn't be
invoked more than 3 times. Also this is useful when testing sequences that rely
on persistent values. For example, the `allPairs` function should cache the
returned values before creating the pairs.
*/
let callCount = () => {
let count = ref(0)
S.foreverWith(() => {
count := count.contents + 1
count.contents
})
}

/**
Create a test that compares two sequences for equality. Converts both sequences
to arrays and then uses the ReScript recursive equality test.
Expand Down Expand Up @@ -1491,6 +1509,149 @@ let delayTests = makeSeqEqualsTests(
],
)

let (combinationTests, permutationTests) = {
let sortLetters = w =>
w->String.split("")->Belt.SortArray.stableSortBy(stringCmp)->Js.Array2.joinWith("")
let sortOutput = combos =>
combos
->S.map(combo => combo->S.reduce("", (sum, i) => sum ++ i)->sortLetters)
->S.sortBy(stringCmp)
->S.toArray
->Js.Array2.joinWith(",")
let sort = words =>
words
->Js.String2.split(",")
->Js.Array2.map(sortLetters)
->Belt.SortArray.stableSortBy(stringCmp)
->Js.Array2.joinWith(",")
let combos = (letters, k) => letters->String.split("")->S.fromArray->S.combinations(k)
let comboString = (letters, k) => combos(letters, k)->S.map(((_, combo)) => combo)->sortOutput
let permutes = (letters, k) => letters->String.split("")->S.fromArray->S.permutations(k)
let permuteString = (letters, k) => permutes(letters, k)->S.map(((_, combo)) => combo)->sortOutput
let comboSamples = makeValueEqualTests(
~title="combinations",
[
(() => ""->comboString(1), ""->sort, ""),
(() => "a"->comboString(1), "a"->sort, ""),
(() => "a"->comboString(2), "a"->sort, ""),
(() => "ab"->comboString(1), "a,b"->sort, ""),
(() => "ab"->comboString(2), "a,b,ab"->sort, ""),
(() => "ab"->comboString(3), "a,b,ab"->sort, ""),
(() => "abc"->comboString(1), "a,b,c"->sort, ""),
(() => "abc"->comboString(2), "a,b,c,ab,ac,bc"->sort, ""),
(() => "abc"->comboString(3), "a,b,c,ab,ac,bc,abc"->sort, ""),
(() => "abc"->comboString(4), "a,b,c,ab,ac,bc,abc"->sort, ""),
(
() =>
"abc"
->combos(4)
->S.filterMap(((size, combo)) => size == 1 ? Some(combo) : None)
->sortOutput,
"a,c,b"->sort,
"check size == 1 in result",
),
(
() =>
"abc"
->combos(4)
->S.filterMap(((size, combo)) => size == 3 ? Some(combo) : None)
->sortOutput,
"abc"->sort,
"check size == 3 in result",
),
(
() =>
callCount()
->S.map(i => i == 1 ? "a" : i == 2 ? "b" : i == 3 ? "c" : "x")
->S.takeAtMost(3)
->S.combinations(3)
->S.map(((_size, combo)) => combo)
->sortOutput,
"a,b,c,ab,ac,bc,abc"->sort,
"values appear cached",
),
],
)
let permuteSamples = makeValueEqualTests(
~title="permutations",
[
(() => ""->permuteString(1), ""->sort, ""),
(() => "a"->permuteString(1), "a"->sort, ""),
(() => "a"->permuteString(2), "a"->sort, ""),
(() => "ab"->permuteString(1), "a,b"->sort, ""),
(() => "ab"->permuteString(2), "a,b,ab,ba"->sort, ""),
(() => "ab"->permuteString(3), "a,b,ab,ba"->sort, ""),
(() => "abc"->permuteString(1), "a,b,c"->sort, ""),
(() => "abc"->permuteString(2), "a,b,c,ab,ac,bc,ba,ca,cb"->sort, ""),
(() => "abc"->permuteString(3), "a,b,c,ab,ac,bc,ba,ca,cb,abc,acb,bac,bca,cab,cba"->sort, ""),
(() => "abc"->permuteString(4), "a,b,c,ab,ac,bc,ba,ca,cb,abc,acb,bac,bca,cab,cba"->sort, ""),
(
() =>
"abc"
->permutes(4)
->S.filterMap(((size, combo)) => size == 1 ? Some(combo) : None)
->sortOutput,
"a,c,b"->sort,
"check size == 1 in result",
),
(
() =>
"abc"
->permutes(4)
->S.filterMap(((size, combo)) => size == 3 ? Some(combo) : None)
->sortOutput,
"abc,acb,bac,bca,cab,cba"->sort,
"check size == 3 in result",
),
(
() =>
callCount()
->S.map(i => i == 1 ? "a" : i == 2 ? "b" : i == 3 ? "c" : "x")
->S.takeAtMost(3)
->S.permutations(3)
->S.map(((_size, combo)) => combo)
->sortOutput,
"a,b,c,ab,ac,bc,ba,ca,cb,abc,acb,bac,bca,cab,cba"->sort,
"values appear cached",
),
],
)
let miscellaneous = (title, f) => [
willThrow(~title, ~expectation="if size < 0 throw", ~f=() => S.range(1, 9)->f(-1)),
willThrow(~title, ~expectation="if size = 0 throw", ~f=() => S.range(1, 9)->f(0)),
valueEqual(
~title,
~expectation="millions - take 10",
~a=() => S.range(1, 1000)->f(1000)->S.takeAtMost(10)->S.last->Belt.Option.isSome,
~b=true,
),
valueEqual(
~title,
~expectation="infinite - take 0",
~a=() => S.foreverWith(() => 1)->f(1000)->S.takeAtMost(0)->S.last,
~b=None,
),
valueEqual(
~title,
~expectation="infinite - take 1",
~a=() => S.foreverWith(() => 1)->f(1000)->S.takeAtMost(4)->S.last->Option.isSome,
~b=true,
),
valueEqual(
~title,
~a=() =>
S.repeatWith(3, () => {Js.Exn.raiseError("oops!")})->f(1000)->S.takeAtMost(0)->S.last,
~b=None,
~expectation="totally lazy",
),
]
let combinationTests =
[comboSamples, miscellaneous("combinations", S.combinations)]->Belt.Array.flatMap(i => i)
let permutationTests =
[permuteSamples, miscellaneous("permutations", S.permutations)]->Belt.Array.flatMap(i => i)
(combinationTests, permutationTests)
}

let sampleFibonacci = {
let fib = Extras__SeqSamples.fibonacci
makeSeqEqualsTests(
Expand Down Expand Up @@ -1582,6 +1743,7 @@ let tests =
allSomeTests,
basicConstructorTests,
chunkBySizeTests,
combinationTests,
compareTests,
concatTests,
consumeTests,
Expand Down Expand Up @@ -1627,6 +1789,7 @@ let tests =
minByMaxByTests,
orElseTests,
pairwiseTests,
permutationTests,
prependTests,
rangeMapTests,
rangeTests,
Expand Down

0 comments on commit a49d8e2

Please sign in to comment.