Format an interval as the band a customer reads - #15
Merged
Merged
Conversation
`format()` returned the interval notation verbatim, so a quote or a document showed "[1,2)" and "(GBP 50000.00,GBP 100000.00]" where a person expected years and pounds. Every other type already formats for a reader — money prints "£1,000,000.00" — and the interval types were the two that leaked their wire form. An interval now reads as its band: "1 – 2", "5 or more", "up to 1,000". Endpoint openness is dropped, because prose has no natural way to say it and no display has needed the distinction; the notation is still exactly one string cast away for anything that needs it back. A half-bounded interval carries the PHP_INT_MIN/PHP_INT_MAX sentinel `fromString` writes for a missing endpoint, so those are read as the absent side they encode rather than printed as if they were endpoints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
robertvansteen
approved these changes
Aug 25, 2026
erikgaal
added a commit
that referenced
this pull request
Aug 25, 2026
Backport of #15 to the 0.6 line, so a consumer still on ^0.6 gets readable intervals without the Interval 2.0 migration that 0.7 onwards requires. An interval now reads as its band: "1 – 2", "5 or more", "up to 1,000", with endpoint openness dropped and the PHP_INT_MIN/PHP_INT_MAX sentinel read as the absent side it encodes. Narrowing the argument through instance_of() also clears the "Cannot cast mixed to string" PHPStan error this branch had inherited from its floating dev dependencies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
IntervalType::format()returned the interval notation verbatim, so anything that displays a formatted value showed the wire form. In the product catalogue's terms API that surfaces as:Every other type in the family already formats for a reader —
MonetaryTypeprints£1,000,000.00— so the interval types were the two leaking their notation into customer-facing output. (gosuperscript/axiom-moneygets the matching change forMonetaryIntervalType.)What
format()now renders the band:[1,2][1,2]1 – 2(1,2)(1,2)1 – 2[50000,100000)[50000,100000)50,000 – 100,000[1.2345,2][1.2345,2]1.2345 – 2[5,)[5,9223372036854775807)5 or more(,1000](-9223372036854775808,1000]up to 1,000(,)(-9223372036854775808,9223372036854775807)anyTwo decisions worth calling out:
[1,2)and[1,2]both read1 – 2. Prose has no natural way to say it, and no display has needed the distinction — the authored labels behind these values say things like "1 year" and "Up to £100,000".(string) $intervalstill gives the exact notation for anything that needs it back, so nothing loses information; it just isn't whatformat()is for.Interval::fromStringencodes an absent endpoint as thePHP_INT_MIN/PHP_INT_MAXsentinel, which the old cast printed as a 19-digit number.format()reads the sentinel as the absent side it encodes — the same conventionMonetaryInterval::__toStringalready relies on.Endpoints are grouped to the thousand and never padded, so an endpoint keeps exactly the decimals it was written with.
Compatibility
format()output is display text, not a parse target —coerce()has never accepted it back (it wants notation, and already refuses£1,000,000.00fromMonetaryType). Anything that persisted or comparedformat()output as an identity would be affected; nothing in this repo does.Verification
composer test— PHPStan (level max), PHPUnit at 100% coverage, Infection at 100% MSI.