# 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).