diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcd16fc..930d22e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ on: - "examples/**" - "LuaSF.lua" - "LuaStat.lua" + - "rockspec/**" - "*.rockspec" - ".github/workflows/ci.yml" @@ -58,6 +59,7 @@ jobs: lua spec/test_sampling.lua lua spec/test_bivariate.lua lua spec/test_shape.lua + lua spec/test_probability.lua - name: Run examples run: | @@ -70,4 +72,5 @@ jobs: lua examples/binomial_coin_flips.lua lua examples/bootstrap_mean.lua lua examples/covariance_correlation.lua - lua examples/skewness_kurtosis.lua \ No newline at end of file + lua examples/skewness_kurtosis.lua + lua examples/probability_helpers.lua diff --git a/.github/workflows/publish-luarocks.yml b/.github/workflows/publish-luarocks.yml index fa7cc25..8c03841 100644 --- a/.github/workflows/publish-luarocks.yml +++ b/.github/workflows/publish-luarocks.yml @@ -6,7 +6,7 @@ on: rockspec: description: "Rockspec file to validate or publish" required: true - default: "rockspec/luasf-0.6.0-1.rockspec" + default: "rockspec/luasf-0.7.0-1.rockspec" type: string publish: description: "Publish to LuaRocks after validation" @@ -50,6 +50,9 @@ jobs: - name: Test shape statistics entry point run: lua -e 'local stats = require("luasf"); print(stats.skewness({1,2,3,4,5})); print(stats.kurtosis({1,2,3,4,5}))' + - name: Test probability helpers entry point + run: lua -e 'local stats = require("luasf"); print(stats.factorial(5)); print(stats.combinations(5,2)); print(stats.permutations_with_repetition(10,4))' + - name: Test LuaSF compatibility entry point run: lua -e 'local stats = require("LuaSF"); print(stats.sumF({1,2,3}))' diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d414f..0a1a800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,52 @@ This project follows a lightweight changelog format inspired by [Keep a Changelo ### Planned -* Continue improving examples and documentation. * Explore a lightweight cross-reference with LuaHMF as a related pure-Lua math helper project. -* Evaluate future probability helpers such as `factorial`, `combinations`, and `permutations`. +* Add more distribution and simulation examples. * Evaluate optional formula-based simple regression summaries while keeping ML workflows outside the current scope. -* Add more distribution examples and simulation-oriented examples. --- -## [0.6.0] - 2026-06-07 +## [0.7.0] - 2026-06-07 + +### Added + +* Implemented `src/luasf/probability.lua` with probability and combinatorics helpers. +* Added `factorial(n)`. +* Added `permutations(n, r)` for ordered selections without repetition. +* Added `combinations(n, r)` for unordered selections without repetition. +* Added `permutations_with_repetition(n, r)` for ordered selections with repetition. +* Added `combinations_with_repetition(n, r)` for unordered selections with repetition. +* Added `permutations_without_repetition(n, r)` as an alias for `permutations(n, r)`. +* Added `combinations_without_repetition(n, r)` as an alias for `combinations(n, r)`. +* Added `nPr(n, r)` as an alias for `permutations(n, r)`. +* Added `nCr(n, r)` as an alias for `combinations(n, r)`. +* Added `multiset_permutations(counts)` for distinct arrangements of repeated item groups. +* Added `spec/test_probability.lua`. +* Added `examples/probability_helpers.lua`. +* Added `rockspec/luasf-0.7.0-1.rockspec`. + +### Changed + +* Updated CI to run probability helper tests and the new probability example. +* Updated the LuaRocks publishing workflow default rockspec path to `rockspec/luasf-0.7.0-1.rockspec`. +* Updated README, API documentation, changelog, and contribution notes for probability helpers. + +### Notes + +Probability helpers distinguish common combinatorics cases: + +* Ordered selections without repetition: `permutations(n, r)` and `nPr(n, r)`. +* Ordered selections with repetition: `permutations_with_repetition(n, r)`. +* Unordered selections without repetition: `combinations(n, r)` and `nCr(n, r)`. +* Unordered selections with repetition: `combinations_with_repetition(n, r)`. +* Repeated item arrangements: `multiset_permutations(counts)`. + +Lua numbers may lose precision for very large combinatorial values. LuaSF intentionally keeps these helpers lightweight and dependency-free instead of adding big integer support. + +--- + +## [0.6.0] - 2026-06-08 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fbff42..04bf184 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ LuaSF aims to provide: * Summary statistics helpers * Shape statistics helpers such as skewness and kurtosis * Bivariate statistics helpers such as covariance and correlation +* Probability and combinatorics helpers * Pseudo-random variable generation * Sampling utilities * A small and readable Lua codebase @@ -153,6 +154,7 @@ lua spec/test_distributions.lua lua spec/test_sampling.lua lua spec/test_bivariate.lua lua spec/test_shape.lua +lua spec/test_probability.lua ``` Run examples: @@ -168,10 +170,45 @@ lua examples/binomial_coin_flips.lua lua examples/bootstrap_mean.lua lua examples/covariance_correlation.lua lua examples/skewness_kurtosis.lua +lua examples/probability_helpers.lua ``` --- +## Probability helper guidelines + +Probability and combinatorics helpers should live in: + +```text +src/luasf/probability.lua +``` + +Tests should live in: + +```text +spec/test_probability.lua +``` + +Examples should live in: + +```text +examples/probability_helpers.lua +``` + +Probability and combinatorics helpers should remain lightweight and formula-based. + +For combinatorics helpers, please be explicit about whether order matters and whether repetition is allowed: + +* `permutations(n, r)` for ordered selections without repetition. +* `combinations(n, r)` for unordered selections without repetition. +* `permutations_with_repetition(n, r)` for ordered selections with repetition. +* `combinations_with_repetition(n, r)` for unordered selections with repetition. +* `multiset_permutations(counts)` for arrangements of repeated item groups. + +Please keep in mind that Lua numbers may lose precision for very large combinatorial values. LuaSF should remain dependency-free and should not add big integer libraries unless the project scope changes significantly. + +--- + ## LuaRocks packaging Rockspec files are kept under: @@ -185,8 +222,8 @@ When adding new internal modules, update the next rockspec draft so LuaRocks kno Before publishing, validate locally or through GitHub Actions: ```bash -luarocks lint rockspec/luasf-0.5.0-1.rockspec -luarocks make rockspec/luasf-0.5.0-1.rockspec +luarocks lint rockspec/luasf-0.7.0-1.rockspec +luarocks make rockspec/luasf-0.7.0-1.rockspec ``` Publishing should remain manual and intentional. @@ -246,6 +283,9 @@ Before opening a pull request, please check: * New behavior includes at least one test. * New modules are included in the rockspec draft when needed. * Code remains readable and dependency-light. +* LuaRocks rockspec files are updated when preparing a package release. +* CI workflows are updated when new tests or examples are added. + --- @@ -258,6 +298,7 @@ Prefer: * Small functions * Minimal dependencies * Compatibility with Lua 5.1+ +* Explicit validation for public helpers * Formula-based helpers when appropriate Avoid: @@ -272,12 +313,6 @@ Avoid: ## Future scope -Potential future additions include: - -* `factorial(n)` -* `combinations(n, r)` -* `permutations(n, r)` - Simple formula-based regression summaries may be considered later, but optimization-based models and ML workflows are outside the current scope. --- diff --git a/README.md b/README.md index 82d2be3..583db29 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ **LuaSF** stands for **Lua Statistics Functions**. -LuaSF is a small, lightweight, pure-Lua library for descriptive statistics, sampling utilities, and pseudo-random variable generation. +LuaSF is a small, lightweight, pure-Lua library for descriptive statistics, shape statistics, bivariate statistics, probability helpers, sampling utilities, and pseudo-random variable generation. -The project started around 2014 and was later published under the MIT License. It has now been revived with compatibility improvements, tests, examples, documentation, a cleaner module structure, additional statistics helpers, sampling utilities, and LuaRocks packaging while preserving the existing public API. +The project started around 2014 and was later published under the MIT License. It has now been revived with compatibility improvements, tests, examples, documentation, a cleaner modular source structure, additional statistics helpers, sampling utilities, probability helpers, and LuaRocks packaging while preserving the existing public API. --- @@ -47,6 +47,7 @@ The project started around 2014 and was later published under the MIT License. I * Basic descriptive statistics * Summary statistics helpers * Bivariate statistics helpers +* Probability and combinatorics helpers * Sampling utilities * Discrete and continuous pseudo-random variables * Compatible with the existing public LuaSF API @@ -111,6 +112,8 @@ print(stats.stddev(values)) -- sample standard deviation print(stats.median(values)) -- 3 print(stats.variance(values)) -- sample variance print(stats.summary(values).count) -- 5 +print(stats.factorial(5)) -- 120 +print(stats.combinations(5, 2)) -- 10 ``` Legacy names are still available: @@ -172,6 +175,23 @@ print(stats.stvF(values)) -- sample standard deviation | `correlation(x, y)` | Pearson correlation coefficient | | `pearson(x, y)` | Alias for `correlation(x, y)` | +### Probability helpers + +| Function | Description | +| --------------------------------------- | ---------------------------------------------------------------------- | +| `factorial(n)` | Factorial of a non-negative integer | +| `permutations(n, r)` | Ordered selections without repetition, also known as `nPr` | +| `combinations(n, r)` | Unordered selections without repetition, also known as `nCr` | +| `permutations_with_repetition(n, r)` | Ordered selections with repetition, calculated as `n^r` | +| `combinations_with_repetition(n, r)` | Unordered selections with repetition, calculated as `C(n + r - 1, r)` | +| `permutations_without_repetition(n, r)` | Alias for `permutations(n, r)` | +| `combinations_without_repetition(n, r)` | Alias for `combinations(n, r)` | +| `multiset_permutations(counts)` | Distinct arrangements of repeated item counts | +| `nPr(n, r)` | Alias for `permutations(n, r)` | +| `nCr(n, r)` | Alias for `combinations(n, r)` | + +> These helpers use Lua numbers. Very large inputs may exceed the practical numeric precision or range of the Lua runtime. + ### Sampling utilities | Function | Description | @@ -341,6 +361,7 @@ LuaSF/ test_sampling.lua test_bivariate.lua test_shape.lua + test_probability.lua examples/ dice_simulation.lua normal_quality_control.lua @@ -352,6 +373,7 @@ LuaSF/ bootstrap_mean.lua covariance_correlation.lua skewness_kurtosis.lua + probability_helpers.lua docs/ api.md .github/ @@ -364,6 +386,7 @@ LuaSF/ luasf-0.4.0-1.rockspec luasf-0.5.0-1.rockspec luasf-0.6.0-1.rockspec + luasf-0.7.0-1.rockspec LuaSF.lua LuaStat.lua README.md @@ -390,6 +413,7 @@ lua spec/test_stats.lua lua spec/test_distributions.lua lua spec/test_sampling.lua lua spec/test_bivariate.lua +lua spec/test_probability.lua ``` --- @@ -406,6 +430,7 @@ lua examples/poisson_arrivals.lua lua examples/binomial_coin_flips.lua lua examples/bootstrap_mean.lua lua examples/covariance_correlation.lua +lua examples/probability_helpers.lua ``` --- @@ -423,17 +448,18 @@ lua examples/covariance_correlation.lua * API documentation * Additional statistics helpers * Summary statistics helpers +* Shape statistics helpers * Bivariate statistics helpers +* Probability helpers * Sampling utilities * Deterministic simulation support * LuaRocks publishing ### Planned -* More distribution and simulation examples * Lightweight cross-reference with LuaHMF -* Future probability helpers such as `factorial`, `combinations`, and `permutations` -* Optional formula-based simple regression summaries +* More distribution and simulation examples +* Optional simple formula-based regression summaries, without turning LuaSF into a machine learning framework --- diff --git a/docs/api.md b/docs/api.md index 44e6c40..fd9561c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -435,6 +435,166 @@ stats.correlation(x, y) --- +## Probability helpers + +Probability helpers are lightweight combinatorics utilities useful for counting cases in probability, simulation, teaching, and small scripts. + +LuaSF distinguishes helpers without repetition and with repetition where appropriate. + +> Lua numbers may lose precision for very large combinatorial values. LuaSF intentionally avoids big integer dependencies to remain small and pure Lua. + +### `factorial(n)` + +Returns the factorial of a non-negative integer. + +```lua +local stats = require("luasf") + +print(stats.factorial(0)) -- 1 +print(stats.factorial(5)) -- 120 +``` + +--- + +### `permutations(n, r)` + +Returns the number of ordered selections of `r` elements from `n` distinct elements without repetition. + +This is commonly known as `nPr`. + +```text +nPr = n! / (n - r)! +``` + +If `r` is omitted, LuaSF treats it as `n` and returns `n!`. + +```lua +local stats = require("luasf") + +print(stats.permutations(5, 3)) -- 60 +print(stats.nPr(5, 3)) -- 60 +``` + +Requirements: + +* `n` must be a non-negative integer. +* `r` must be a non-negative integer. +* `r <= n`. + + +Alias: + +```lua +stats.permutations_without_repetition(n, r) +``` + +--- + +### `combinations(n, r)` + +Returns the number of unordered selections of `r` elements from `n` distinct elements without repetition. + +This is commonly known as `nCr`. + +```text +nCr = n! / (r! * (n - r)!) +``` + +```lua +local stats = require("luasf") + +print(stats.combinations(5, 3)) -- 10 +print(stats.nCr(5, 3)) -- 10 +``` + +Requirements: + +* `n` must be a non-negative integer. +* `r` must be a non-negative integer. +* `r <= n`. + + +Alias: + +```lua +stats.combinations_without_repetition(n, r) +``` + + +--- + +### `permutations_with_repetition(n, r)` + +Returns the number of ordered selections of `r` elements from `n` possible values with repetition allowed. + +This is equivalent to: + +```text +n^r +``` + +```lua +local stats = require("luasf") + +print(stats.permutations_with_repetition(5, 3)) -- 125 +print(stats.permutations_with_repetition(10, 4)) -- 10000 +``` + +Requirements: + +* `n` must be a non-negative integer. +* `r` must be a non-negative integer. + + +This is useful for cases such as PIN-like sequences, codes, or ordered choices where values can repeat. + +--- + +### `combinations_with_repetition(n, r)` + +Returns the number of unordered selections of `r` elements from `n` possible values with repetition allowed. + +This is equivalent to: + +```text +C(n + r - 1, r) +``` + +```lua +local stats = require("luasf") + +print(stats.combinations_with_repetition(5, 3)) -- 35 +``` + +Requirements: + +* `n` must be a positive integer. +* `r` must be a non-negative integer. + +--- + +### `multiset_permutations(counts)` + +Returns the number of distinct arrangements of repeated item groups. + +`counts` is an array containing how many times each repeated group appears. + +This is equivalent to: + +```text +n! / (a! * b! * c! * ...) +``` + +where `n` is the sum of all counts. + +```lua +local stats = require("luasf") + +print(stats.multiset_permutations({3, 2, 1})) -- 60 +``` + +--- + ## Sampling utilities ### `choice(array)` @@ -938,6 +1098,23 @@ stats.range stats.iqr stats.percentile stats.summary +stats.central_moment +stats.skewness +stats.kurtosis +stats.excess_kurtosis +stats.covariance +stats.correlation +stats.pearson +stats.factorial +stats.permutations +stats.combinations +stats.permutations_with_repetition +stats.combinations_with_repetition +stats.permutations_without_repetition +stats.combinations_without_repetition +stats.multiset_permutations +stats.nPr +stats.nCr stats.choice stats.shuffle stats.sample diff --git a/examples/probability_helpers.lua b/examples/probability_helpers.lua new file mode 100644 index 0000000..5c0649f --- /dev/null +++ b/examples/probability_helpers.lua @@ -0,0 +1,19 @@ +local stats = require("luasf") + +print("Probability helper examples") +print("Factorial 5!:", stats.factorial(5)) + +print("Ordered selections without repetition") +print("Race podiums with 8 runners and 3 medals:", stats.permutations(8, 3)) + +print("Ordered selections with repetition") +print("4-digit PINs using digits 0-9:", stats.permutations_with_repetition(10, 4)) + +print("Unordered selections without repetition") +print("Committees of 3 people from 10 candidates:", stats.combinations(10, 3)) + +print("Unordered selections with repetition") +print("3 scoops from 5 ice cream flavors:", stats.combinations_with_repetition(5, 3)) + +print("Distinct permutations of repeated items") +print("Letters in MISSISSIPPI-style counts {4, 4, 2, 1}:", stats.multiset_permutations({4, 4, 2, 1})) diff --git a/rockspec/luasf-0.7.0-1.rockspec b/rockspec/luasf-0.7.0-1.rockspec new file mode 100644 index 0000000..33c9da9 --- /dev/null +++ b/rockspec/luasf-0.7.0-1.rockspec @@ -0,0 +1,41 @@ +package = "luasf" +version = "0.7.0-1" + +source = { + url = "git://github.com/HubertRonald/LuaSF.git", + tag = "v0.7.0" +} + +description = { + summary = "Lua Statistics Functions", + detailed = [[ +LuaSF is a lightweight, pure-Lua library for descriptive statistics, +shape statistics, bivariate statistics, probability helpers, sampling utilities, +simulation examples, and random variable generation. + ]], + homepage = "https://github.com/HubertRonald/LuaSF", + license = "MIT", + maintainer = "Hubert Ronald" +} + +dependencies = { + "lua >= 5.1" +} + +build = { + type = "builtin", + modules = { + luasf = "src/luasf.lua", + ["luasf.core"] = "src/luasf/core.lua", + ["luasf.validation"] = "src/luasf/validation.lua", + ["luasf.rng"] = "src/luasf/rng.lua", + ["luasf.descriptive"] = "src/luasf/descriptive.lua", + ["luasf.shape"] = "src/luasf/shape.lua", + ["luasf.sampling"] = "src/luasf/sampling.lua", + ["luasf.distributions"] = "src/luasf/distributions.lua", + ["luasf.bivariate"] = "src/luasf/bivariate.lua", + ["luasf.probability"] = "src/luasf/probability.lua", + LuaSF = "LuaSF.lua", + LuaStat = "LuaStat.lua" + } +} diff --git a/spec/test_probability.lua b/spec/test_probability.lua new file mode 100644 index 0000000..7929b90 --- /dev/null +++ b/spec/test_probability.lua @@ -0,0 +1,135 @@ +local luaunit = require("luaunit") +local stats = require("luasf") + +TestProbability = {} + +function TestProbability:test_factorial_zero() + luaunit.assertEquals(stats.factorial(0), 1) +end + +function TestProbability:test_factorial_positive_integer() + luaunit.assertEquals(stats.factorial(5), 120) +end + +function TestProbability:test_factorial_rejects_negative_values() + luaunit.assertError(function() + stats.factorial(-1) + end) +end + +function TestProbability:test_factorial_rejects_non_integer_values() + luaunit.assertError(function() + stats.factorial(3.5) + end) +end + +function TestProbability:test_permutations_requires_r_less_than_or_equal_to_n() + luaunit.assertError(function() + stats.permutations(3, 4) + end) +end + +function TestProbability:test_combinations_requires_r_less_than_or_equal_to_n() + luaunit.assertError(function() + stats.combinations(3, 4) + end) +end + +function TestProbability:test_combinations_with_repetition_requires_positive_n() + luaunit.assertError(function() + stats.combinations_with_repetition(0, 2) + end) +end + +function TestProbability:test_permutations_without_repetition() + luaunit.assertEquals(stats.permutations(5, 2), 20) +end + +function TestProbability:test_permutations_without_repetition_all_items() + luaunit.assertEquals(stats.permutations(5, 5), 120) +end + +function TestProbability:test_combinations_without_repetition() + luaunit.assertEquals(stats.combinations(5, 2), 10) + luaunit.assertEquals(stats.combinations_without_repetition(5, 2), 10) + luaunit.assertEquals(stats.combinations(5, 0), 1) + luaunit.assertEquals(stats.combinations(5, 5), 1) +end + +function TestProbability:test_permutations_without_repetition_zero_selected() + luaunit.assertEquals(stats.permutations(5, 0), 1) +end + +function TestProbability:test_permutations_without_repetition_rejects_r_greater_than_n() + luaunit.assertError(function() + stats.permutations(3, 4) + end) +end + +function TestProbability:test_combinations_without_repetition_symmetric_case() + luaunit.assertEquals(stats.combinations(10, 3), 120) + luaunit.assertEquals(stats.combinations(10, 7), 120) +end + +function TestProbability:test_combinations_without_repetition_zero_selected() + luaunit.assertEquals(stats.combinations(5, 0), 1) +end + +function TestProbability:test_combinations_without_repetition_all_items() + luaunit.assertEquals(stats.combinations(5, 5), 1) +end + +function TestProbability:test_combinations_without_repetition_rejects_r_greater_than_n() + luaunit.assertError(function() + stats.combinations(3, 4) + end) +end + +function TestProbability:test_permutations_with_repetition() + luaunit.assertEquals(stats.permutations_with_repetition(4, 3), 64) +end + +function TestProbability:test_permutations_with_repetition_zero_selected() + luaunit.assertEquals(stats.permutations_with_repetition(4, 0), 1) +end + +function TestProbability:test_permutations_with_repetition_empty_set_zero_selected() + luaunit.assertEquals(stats.permutations_with_repetition(0, 0), 1) +end + +function TestProbability:test_permutations_with_repetition_empty_set_positive_selection() + luaunit.assertEquals(stats.permutations_with_repetition(0, 3), 0) +end + +function TestProbability:test_combinations_with_repetition() + luaunit.assertEquals(stats.combinations_with_repetition(5, 3), 35) +end + +function TestProbability:test_combinations_with_repetition_zero_selected() + luaunit.assertEquals(stats.combinations_with_repetition(4, 0), 1) +end + +function TestProbability:test_combinations_with_repetition_single_item() + luaunit.assertEquals(stats.combinations_with_repetition(1, 5), 1) +end + +function TestProbability:test_combinations_with_repetition_rejects_zero_items() + luaunit.assertError(function() + stats.combinations_with_repetition(0, 2) + end) +end + +function TestProbability:test_multiset_permutations() + luaunit.assertEquals(stats.multiset_permutations({3, 2, 1}), 60) + luaunit.assertEquals(stats.multiset_permutations({2, 2}), 6) +end + +function TestProbability:test_nPr_alias() + luaunit.assertEquals(stats.nPr(5, 2), 20) +end + +function TestProbability:test_nCr_alias() + luaunit.assertEquals(stats.nCr(5, 2), 10) +end + +os.exit(luaunit.LuaUnit.run()) diff --git a/src/luasf/probability.lua b/src/luasf/probability.lua index 790f982..b86cf53 100644 --- a/src/luasf/probability.lua +++ b/src/luasf/probability.lua @@ -1,13 +1,110 @@ ---[[ -Probability helpers placeholder. +local module_name = ... or "luasf.probability" +local prefix = module_name:match("^src%.") and "src.luasf." or "luasf." -This module is intentionally small for now. Future versions may include -factorial, combinations, permutations, and related probability helpers. +local validation = require(prefix .. "validation") -Those helpers should be added carefully to avoid overflow and to keep LuaSF -focused on lightweight statistics, probability, and simulation utilities. -]] +local min = math.min local M = {} +local function factorial(n) + validation.assert_non_negative_integer(n, "n") + + local result = 1 + + for i = 2, n do + result = result * i + end + + return result + end + +-- Ordered selections without repetition. +-- Equivalent to nPr = n! / (n - r)!. +-- If r is omitted, this returns n!. +local function permutations(n, r) + validation.assert_non_negative_integer(n, "n") + r = r == nil and n or r + validation.assert_non_negative_integer(r, "r") + assert(r <= n, "r must be less than or equal to n") + + local result = 1 + + for i = n - r + 1, n do + result = result * i + end + + return result + end + + +-- Ordered selections with repetition. +-- Equivalent to n^r. +local function permutations_with_repetition(n, r) + validation.assert_non_negative_integer(n, "n") + validation.assert_non_negative_integer(r, "r") + + return n ^ r + end + +-- Unordered selections without repetition. +-- Equivalent to nCr = n! / (r! * (n - r)!). +local function combinations(n, r) + validation.assert_non_negative_integer(n, "n") + validation.assert_non_negative_integer(r, "r") + assert(r <= n, "r must be less than or equal to n") + + r = min(r, n - r) + + local result = 1 + + for i = 1, r do + result = result * (n - r + i) / i + end + + return result + end + +-- Unordered selections with repetition. +-- Equivalent to C(n + r - 1, r). +local function combinations_with_repetition(n, r) + validation.assert_non_negative_integer(n, "n") + validation.assert_non_negative_integer(r, "r") + assert(n >= 1, "n must be greater than or equal to 1") + + return combinations(n + r - 1, r) + end + +-- Distinct permutations of a multiset represented by repeated counts. +-- Example: counts {3, 2, 1} -> 6! / (3! * 2! * 1!) = 60. +local function multiset_permutations(counts) + validation.assert_non_empty_array(counts, "counts") + + local total = 0 + local denominator = 1 + + for _, count in ipairs(counts) do + validation.assert_non_negative_integer(count, "count") + total = total + count + denominator = denominator * factorial(count) + end + + return factorial(total) / denominator + end + + M.factorial = factorial + M.permutations = permutations -- without_repetition + M.combinations = combinations -- without_repetition + M.permutations_with_repetition = permutations_with_repetition + M.combinations_with_repetition = combinations_with_repetition + M.permutations_without_repetition = permutations + M.combinations_without_repetition = combinations + M.multiset_permutations = multiset_permutations + + -- Common combinatorics aliases. + M.nPr = permutations + M.nCr = combinations + + + return M \ No newline at end of file diff --git a/src/luasf/validation.lua b/src/luasf/validation.lua index dcdbfa3..e2af90e 100644 --- a/src/luasf/validation.lua +++ b/src/luasf/validation.lua @@ -1,5 +1,7 @@ local M = {} +local floor = math.floor + function M.assert_number(value, name) name = name or "value" assert(type(value) == "number", name .. " must be a number") @@ -51,4 +53,20 @@ function M.assert_same_length_numeric_arrays(x, y, x_name, y_name) M.assert_numeric_array(y, y_name) end +function M.assert_integer(value, name) + name = name or "value" + M.assert_number(value, name) + assert(value == floor(value), name .. " must be an integer") +end + +function M.assert_non_negative_integer(value, name) + M.assert_integer(value, name) + assert(value >= 0, name .. " must be greater than or equal to 0") +end + +function M.assert_positive_integer(value, name) + M.assert_integer(value, name) + assert(value >= 1, name .. " must be greater than or equal to 1") +end + return M \ No newline at end of file