Skip to content

Format an interval as the band a customer reads - #15

Merged
erikgaal merged 1 commit into
mainfrom
humanise-formatted-intervals
Aug 25, 2026
Merged

erikgaal merged 1 commit into
mainfrom
humanise-formatted-intervals

Conversation

@erikgaal

Copy link
Copy Markdown
Member

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:

Years of experience in the industry   [1,2)
Estimated annual turnover             (GBP 50000.00,GBP 100000.00]

Every other type in the family already formats for a reader — MonetaryType prints £1,000,000.00 — so the interval types were the two leaking their notation into customer-facing output. (gosuperscript/axiom-money gets the matching change for MonetaryIntervalType.)

What

format() now renders the band:

value before after
[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) any

Two decisions worth calling out:

  • Endpoint openness is dropped. [1,2) and [1,2] both read 1 – 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) $interval still gives the exact notation for anything that needs it back, so nothing loses information; it just isn't what format() is for.
  • A missing endpoint is recognised. Interval::fromString encodes an absent endpoint as the PHP_INT_MIN/PHP_INT_MAX sentinel, which the old cast printed as a 19-digit number. format() reads the sentinel as the absent side it encodes — the same convention MonetaryInterval::__toString already 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.00 from MonetaryType). Anything that persisted or compared format() 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.

`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>
@erikgaal
erikgaal merged commit 1b1e0ef into main Aug 25, 2026
3 checks passed
@erikgaal
erikgaal deleted the humanise-formatted-intervals branch August 25, 2026 13:59
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants