Index
# mmo.math — overview for AI code generation `mmo.math` is Marc Mouret's symbolic math library for MMOFramework (PHP package `mmo.math`, PHP namespace `MMO\math`, depends on `mmo.core`). It is **not** a plain calculator: `\MMO\math\plus()`, `minus()`, `multiply()`, `divide()`, `power()` and the expression parser `\MMO\math\parse()` build a tree of `iNumber` nodes — plain `int|float` where they can be reduced, a `Number` where a value is unknown (a "variable"), or an `Operation`/`ArithmeticOperation`/`ConstraintOperation`/`LogicOperation` node where they can't be reduced yet. That tree carries **units** (compile-time-checked at each `+`/`-`), can be turned into a **`Polynome`** and solved, and its unknowns can be read back as **`Domain`/`Interval`** ranges. On top of this it has a small **xlsx formula evaluator** (`MMO\math\xlsx\*`) that plugs into `mmo.core`'s spreadsheet documents. Like `mmo.core`, the entry points are plain namespaced functions under `\MMO\math\*` (`functions.php`), not a service class — structured data lives in classes under `MMO\math\helpers\*`, `MMO\math\operations\*` and `MMO\math\enums\*`. All of it is loaded by `MMO\math`'s own `bootstrap.php`, wired into the framework's `\MMO\inc('mmo.math')` package-loading mechanism (`mmo.json` names no `"bootstrap"`, so `PackageManifest`'s default, `src/bootstrap.php`, applies). The `MMO\math\xlsx\*` classes are **not** loaded by that bootstrap directly — they're registered with `Autoload::OnNamespace('MMO\\math\\xlsx', ...)` and only `require`d the first time code actually references that namespace. ## The five things that matter most 1. **Arithmetic is lazy and symbolic — always unwrap the result.** `\MMO\math\plus($a, $b)` (and `minus`/ `multiply`/`divide`/`power`) returns a plain `int|float` *only* when it could fully reduce; otherwise it returns an `ArithmeticOperation` node (e.g. adding an unset `Number` variable to `3` gives you back an object, not a float). Don't assume the return type — call `->numberValue()` (or `\MMO\math\number($result)`) to pull out a scalar, and expect `null` back if the tree still contains an unset variable. 2. **`\MMO\math\parse()` requires an explicit `*` before a unit token — `5*[custom]` / `3*m`, never `5[custom]` / `3m`.** Verified directly: `parse('5[banane] + 2[banane]')` throws `eLogic: units [...] is not compatible with [...]` because the tokenizer treats the number and the bracketed/bare unit as two unrelated adjacent tokens with no implicit multiplication between them. `parse('5*[banane] + 2*[banane]')` works and yields a value of `7` with `units() == [banane]`. Built-in unit tokens work the same way: `'3*m + 2*m'` parses fine, `'3m + 2m'` does not. 3. **A bare `Number` prints as `$?` — only named variables get a real `__toString()`.** `Number::__toString()` defers to an internal `UnitNumber` that's created lazily by `setName()` (which `parse()` calls automatically for every `$x`-style token it creates). `new Number(100, cUnit::Meter)` — a plain literal, never named — has no name, so `(string)$number` is `'$?'`, not `'100m'`. Don't build user-facing output by string-casting a `Number` unless you know it went through `parse()` or an explicit `->setName()`; read `->numberValue()` and `->units()` instead. 4. **Constraint and logic operators return a "fulfilled score" (`1`/`0`), not a bool.** `cConstraintOperator` (`<= < >= > != =`) and `cLogicOperator` (`&& || !`) are `__invoke`-able, and their result is `1` (fulfilled) or `0` (not) — once `$x` is set to `5`, `\MMO\math\parse('$x > 3')->numberValue()` is `1`, not `true` (a constant `parse('5 > 3')` is reduced right away and comes back as the plain string `'1'`, which has no `->numberValue()` to call). `cLogicOperator::And`/`Or`/`Not` are built on `\MMO\math\min()`/`max()` rather than short-circuit boolean logic, and those throw `eLogic` today (see Known gaps), so evaluating `&&`/`||`/`!` (`parse('5 > 3 && 2 < 1')`, a `LogicOperation`'s `->numberValue()`) throws too. What does work is the one place that turns a `LogicOperation` tree into an actual `bool` plus a per-term breakdown: `LogicOperation::validate(array &$scores = [])`. 5. **`Polynome::solve()` only handles degree 1 and 2; `System` is mostly unfinished.** `Polynome::solve()` implements `ax + b = 0` and `ax² + bx + c = 0` (confirmed: `System::add('4/($x^2) - 1 = 0')` → `getPolynome('x', 0)->solve()` returns `[-2, 2]`); any other degree throws `eLogic('cannot solve for degree [...]')`. `System` itself only really supports registering named variables and expressions today (`add()`, `getNumber()`, `getPolynome()`) — its `solve()` and `getVariablesComplexity()` exist but throw `eLogic` ("not implemented yet"), and its constraint-scoring methods are present only as commented-out code in `src/helpers/System.php`. Don't call `System::solve()` expecting a result. Also ignore `index.php` at the package root — it's Marc's personal scratch file and references classes (`Variable`, `Solver`, `UnitsNumber`, …) that don't exist anywhere in `src/`. ## Building an expression The normal way to build a math expression is `\MMO\math\parse()`, not constructing `Operation` objects by hand: ```php $vars = []; $exp = \MMO\math\parse('$x + 3 * 2', null, [], $vars); // iNumber|string echo $exp; // "$x+6" (still symbolic: $x is unset) $vars['x']->setValue(4); // $vars['x'] is the auto-created Number for "$x" echo $exp->numberValue(); // 10 ``` `parse(string|array $exp, ?callable $variableFactory = null, array $customOperators = [], array &$vars = [])`: - `$variableFactory`: `function(string $name): ?iNumber` called for every `$name` token; return `null` (or omit the callback) to let `parse()` create a plain `Number` for it. `System::add()` uses this to register/reuse its own variables, and `xlsx\Formulae` uses it to resolve `$0`, `$1`, … back to `CellRef`s. - `$customOperators`: a map of `name => callable|iOperator` recognized as `name(arg1, arg2, ...)` in the expression — this is how the xlsx evaluator adds spreadsheet functions like `IF(...)` (see `MMO\math\xlsx\functions.php::registerFunction()`). - `$vars`: filled with every `$name` variable encountered, keyed by name. Tokens recognized inside the expression string: numbers (`5`, `3.2`, `2e10`), `$name` variables, `[custom]` user-defined units, `"quoted strings"`, parentheses/commas for grouping and function calls, and the operators from the three enums below (`cArithmeticOperator`, `cConstraintOperator`, `cLogicOperator`) plus built-in unit tokens from `cTime`/`cUnit`/`cPrefix+cUnit` (e.g. `km`, `hour`) — remembering rule #2 above about the required `*`. ## Units and dimensional analysis Units are tracked automatically through arithmetic and checked at each `+`/`-`: ```php use MMO\math\helpers\{Number, Units, UnitVector}; use MMO\math\enums\cUnit; $distance = new Number(100, cUnit::Meter); $time = new Number(4, cUnit::Second); // + / - between incompatible units throws eLogic at construction time: // \MMO\math\plus($distance, new Number(1, cUnit::Second)); // eLogic: units not compatible $km = new Units(new UnitVector(cUnit::Meter, 1, 1000)); // 1000 * meter^1 $inKm = $distance->convert($km); echo $inKm->numberValue(); // 0.1 ``` - `Number(float|int|null $value, string|iUnit|UnitVector|Units|null $units)` — `$value` is `null` for an unset variable. `$units` can be a raw string (a *custom* unit, printed as `[name]`), a `cUnit`/`cTime` case, a `UnitVector` (unit + power + factor, e.g. an SI-prefixed unit), or a full `Units` collection. - `Units` is the collection type (`ArrayAccess`/`Countable`/`IteratorAggregate` over `UnitVector`s); `cUnit` composites (`Newton`, `Joule`, `Pascal`, …) decompose to SI primitives via `primitiveUnits()`, which is what `Units::standard()`/`convertFactor()`/`isCompatible()` use to check and convert between units. `Units::Compatible($a, $b)` is the static helper `ArithmeticOperation`'s constructor uses to validate `+`/`-`. - `cTime` (`sec`..`year`) is a separate unit axis from `cUnit`'s SI `Second`, with its own `timeFactor()` / `toDateInterval()` — used by `Number::FromDateInterval()` and `Event`. ## Domains and intervals `Interval` (a single `[start,end]` float range, immutable, with independent `includeStart`/`includeEnd`) and `Domain` (an ordered set of non-overlapping `Interval`s, `ArrayAccess`/`IteratorAggregate`) implement set algebra: `union()`, `intersect()`, `exclude()`, `inverse()`, plus `contains()`/`overlap()`/`isEqual()`. Both are driven by `Interval::position()`, which returns a bitmask (`Interval::LEFT`/`RIGHT`/`INSIDE`/`CONTAIN`/ `LEFT_OVERFLOW`/`RIGHT_OVERFLOW`) describing how one interval sits relative to another — that's the primitive every other method switches on. ```php use MMO\math\helpers\{Domain, Interval}; $d = new Domain([1, 2], [3, 4]); // two disjoint closed intervals: [1,2]u[3,4] $d->contains(3.5); // true $d->contains(3.0); // true (bounds are closed unless told otherwise) $d->contains(2.5); // false (the gap between them) $d->contains(new Domain([1.5, 2], [3, 3.5])); // true: every interval of the value is inside $d->contains(new Interval(1.5, 3.5)); // false: it crosses the gap $d->contains(new Domain()); // false: an empty value is never contained $open = new Domain([1, 2, false, false]); // ]1,2[: [start, end, includeStart, includeEnd] $open->contains(1.0); // false $open->contains(new Interval(1, 2, false, false)); // true ``` `Domain::Columns(Domain ...$lines)` is a distinct, demonstrated feature: given several `Domain`s representing "columns" found on different lines (see `demo/columns.php`, which renders the result as SVG), it computes the common column layout across all of them — reconciling intervals that don't align cell-for-cell between lines. `Domain::match(iSimpleDomain $value, int $flags = self::ALLOW_ALL)` (flags: `ALLOW_VALUE_SPAN`, `ALLOW_THIS_SPAN`, `ALLOW_OVERFLOW`, `ALLOW_EMPTY`, or `ALLOW_ALL`/`ALLOW_SPAN`/`ALLOW_SPAN_OVERFLOW` combinations) matches another domain's intervals against this domain's cells and returns, per cell, which value-intervals landed in it (or `null` if nothing satisfies the requested flags) — see the extensive `@example` block in `src/helpers/Domain.php` and `tests/helpers/TestDomain.php::dataMatch()` for the exact matching rules per flag combination. ## Polynomials and solving `Polynome` collects an expression's terms as `powers[power][] = UnitVector|Units|number` around one `Number` treated as "the variable" (internally, every `Number` used this way gets a `UnitNumber` — a single-letter generated name like `a`, `b`, `c` unless you `setName()` it — so `Polynome` can track its powers as if it were a regular unit). ```php use MMO\math\helpers\System; $system = new System(); $system->add('4/($x^2) - 1 = 0'); // registers $x, adds a ConstraintOperation $poly = $system->getPolynome('x', 0); // Polynome for expression #0, around variable 'x' echo $poly->toNumber(); // (($x^-2)*4)-1 $poly->solve(); // [-2, 2] (degree-2 formula) ``` Get a `Polynome` from an `ArithmeticOperation` or `ConstraintOperation` via `->toPolynome($number)` (a `ConstraintOperation` builds it as *left-hand-side minus right-hand-side*, so `solve()` finds where the constraint is exactly satisfied). Only `degree() === 1` and `degree() === 2` are solvable; anything else throws `eLogic`. `Polynome::isSimple()` tells you whether every term is really in terms of the same single variable (multi-variable polynomials build fine but aren't solvable by `solve()`). ## Calendar events `Event(DateTimeInterface|int|null $start, DateTimeInterface|int|null $end, DateInterval|int|float|string|null $duration)` holds `start`/`end`/`duration` as three `Number`s and derives whichever one is missing from the other two (`getStartDate()`, `getEndDate()`, `getDateInterval()` — each falls back to computing itself from the other two if its own value isn't set). `getInterval()` returns an `Interval` covering `[start, end]` with `$this` attached to it via `Interval`'s `SplObjectStorage`-backed `attach()`/`storageObjects()` (so code walking a `Domain` built from many events' intervals can recover the originating `Event`). ## The xlsx formula evaluator `MMO\math\xlsx\*` (lazily loaded — see above) extends `mmo.core`'s xlsx document support: - `Formulae extends \MMO\core\documents\xlsx\Formulae` — `setExpression()` rewrites cell/range references (`REGEXP_RANGE` from the core class) into `$0`, `$1`, … and parses the result with `MMO\math\xlsx\parse()` (which forwards to `\MMO\math\parse()` with the registered function table); `execute()` unwinds any `Operation` chain by repeatedly calling `->execute()` until it's a plain value. - `CellRef implements iNumber` — resolves a single reference against a `Sheet` (`$sheet->getCellValue($ref)`) each time it's read. - `registerFunction(string $name, ?callable $fn)` (in `xlsx/functions.php`) adds/removes a spreadsheet function usable as `NAME(arg1, arg2)` in a formula; the package registers `IF(cond, true, false)` out of the box. ## Known gaps (don't build on these) - `\MMO\math\max()`, `min()` and `size()` (`functions.php`) throw `eLogic` ("... is not implemented yet") — their implementations are entirely commented out. `cLogicOperator` (`&&`/`||`/`!`) is built on `min()`/`max()`, so it throws too. - `NumberSet` (`src/helpers/NumberSet.php`) is never `require`d by `bootstrap.php` (that line is commented out) — it exists on disk but isn't part of the loaded package today. - `System::solve()` and `getVariablesComplexity()` throw `eLogic` ("not implemented yet"), and its constraint/scoring methods (`startConstraint()`, `score()`, `isValid()`, …) are present only as commented-out code; only `add()`/`getNumber()`/`getPolynome()` currently work. - `Interval::position()` ignores the *value's* own open bounds, and `Interval::contains()`, `Interval::overlap()` and `Domain::overlap()` decide through it: `[1,2[` does not contain `[1.5,2[`, `]1,2[` does not contain itself, and `[2,3]` overlaps `[1,2[`. A float value is unaffected, and so is `Domain::contains()`, which compares bounds itself. - A union can be stored as two touching intervals: `new Domain([2, 3], [1, 2, true, false])` holds `[1,2[u[2,3]` and the reverse order `[1,3]`, so `isEqual()` tells them apart (`Domain::contains()` treats them as one).