# Every standard type meets one contract

Every standard type comes with the same baseline of operations, so once
you know how one type is built, read, sent, stored and shown, you know
them all. Some types add operations of their own on top
([Extras beyond the baseline](every-standard-type-meets-one-contract.md#extras-beyond-the-baseline)).

| Baseline | What it is | Where a type has none |
|---|---|---|
| Construct and validate | From its parts (`PostalCode(raw)`, `Money` from `money.from_minor`), and from text with `text.parse(raw, as: T)`; each failure named by the rule it breaks, `malformed` for text that isn't the type at all | Never: every type has both |
| `format` for people | `text.format(value, locale:)`, giving `PresentationText`, through CLDR where the locale matters (an address's line order, a name's order, a quantity's unit) | Every type has one |
| Canonical text | `text.from(value)`: E.164, ISO codes, `#rrggbb`, ISO 8601, `pkg:` URLs; the form `text.parse` reads back | Types with no single text form (`PostalAddress`, `PersonName`); asking is a compile error |
| JSON | One shape, read and written by `json.read`, `json.write` and every route | Never |
| Storage | One shape: a column type, or named columns for an object (`Money`'s two) | Never |
| `==`, and order | `==` on every type; `<` and `sort_by` where the type is ordered (quantities of one kind, `Money` of one currency, times, `SemanticVersion`, `Percentage`) | Order only where it means something |
| Conversions at named edges | `geo.from_lng_lat`, `units.in`, `Isbn` 10 to 13, `MaskedCardNumber` from a `PaymentCardNumber` | Only where the type has another form |

Each of `text.parse`, `text.format` and `text.from` is one operation over
every standard type, as `json.read` is one over every input type, so the
contract adds 2 names, not 700.

A standard type may take a type argument (`Range<T>`), be a choice of
other types (`Discount`, an [enum with data](enums.md)),
be built on a built-in type with bounds of its own (`TaxRate`), or be a
named collection (`RichText` a list, `LocalisedText` a map). A named
collection is built, sent and read as one your app declares is. Every
standard object and choice is stored in named columns laid out from its
parts ([Every boundary through the contract](every-boundary-through-the-contract.md)).

`jadpo suggest` uses each standard type's shape and its signals, such as
conversion constants, the words of names, format literals and validation
texts, to find code that hand-rolls the type
([Suggestions](suggestions.md)).

In this example a form sends a quantity as text, an order's time goes
into a log line as its canonical text, and a day, a duration and a zone
are shown to people in their own words:

```jadpo
type OrderedAt = Instant {}

type LogText = Text {}

type FormText = Text {}

type Quantity = Int {
    min: 1
    max: 12
}

type QuantityUnreadable = Failure {
    kind: FailureKind.invalid_value
}

type QuantityTooLarge = Failure {
    kind: FailureKind.invalid_value
}

/// The quantity a form sent: text that may not be a number at all
/// (`malformed`), or a number outside `Quantity`'s rules.
function quantity_from(raw: FormText) fails QuantityUnreadable, QuantityTooLarge -> Quantity {
    var quantity: Quantity = attempt text.parse(raw, as: Quantity) {
        malformed: QuantityUnreadable
        invalid: QuantityTooLarge
    }
    return quantity
}

/// When an order was placed, as a log line writes it.
function placed_text(at: OrderedAt) -> LogText {
    return LogText(text.from(at))
}
```

```jadpo
test "standard values are read, written and shown one way" {
    var ordered_quantity: Quantity = attempt quantity_from("4")
    assert ordered_quantity == 4
    assert quantity_from("13") fails QuantityTooLarge
    assert quantity_from("four") fails QuantityUnreadable
    assert placed_text("2026-10-25T09:30:00Z") == "2026-10-25T09:30:00Z"
    assert text.from(Zone.europe_london) == "Europe/London"
    assert text.format(Weekday.monday, locale: Locale.fr_fr) == "lundi"
    assert text.format(90m, locale: Locale.en_gb) == "1 hour, 30 minutes"
    assert text.format(Locale.fr_fr, locale: Locale.en_gb) == "French (France)"
}
```

## Extras beyond the baseline

Many groups of types have operations beyond the baseline. Each has its
full table under [Operations](standard-library-operations.md):

| Group | Extras | Count |
|---|---|---|
| Money and payments | `money.from_minor`, `of`, `minor`, `currency`, `sum`, `allocate`, `split`, `convert` (with a given `ExchangeRate`, never fetched), `mask` (a `MaskedCardNumber` from a `PaymentCardNumber`), `apply_discount` (a `Discount` off a `Price`) | 10; arithmetic by the operator table |
| Physical quantities, `DataSize`, `Bitrate` | `units.of`, `in`, `sum`; operators within a kind and the defined products and quotients | 3 |
| Time and calendar | The 21 from [Configuration and time](configuration-and-time.md), plus `temporal.occurrences`, `temporal.open_at` and `temporal.period_between`; `CalendarPeriod` arithmetic by the operator table | 3 new |
| Ranges (`Range<T>`, `DateRange`, `InstantRange`) | `range.contains`, `range.overlaps`, `range.intersection` | 3 |
| Colour | `colour.from_hsl`, `from_oklch`, `mix`, `with_lightness`, `contrast` (WCAG 2) | 5 |
| Security | `crypto` as tabled below, with `sign`, `verify_signature`, `verify_jwt` and `verify_totp` | 10 |
| Web | `web.with_query(url, entries)`, `web.resolve(base, relative)`, `web.cidr_contains(range, address)` | 3 |
| Software | `semver.satisfies(version, range)` | 1 |
| Media | `media.fit(dimensions, within:)`, keeping the aspect ratio | 1 |
| Text formats | `markup.to_html`, `rich_to_html`, `to_plain_text`; `text.pick` with fallback. `Html` is always safe: its construction sanitises | 4 |
| Geometry | `geo.distance`, `length`, `bearing`, `area`, `intersects`, `contains`, `within`, `centroid`, `bounding_box`, the constructions and WKT's two | [Geospatial](geospatial.md) |
| AI (`Vector`, `Embedding`, `ChatMessage`) | `ai.generate`, `stream`, `embed`, `embed_all`, `distance` | [AI](ai.md) |
| People, places, identity numbers, commerce codes, health, travel, other | Baseline only: each identifier's check digit or pattern is its construction | |

## Rules across a type's parts

Some standard types have parts that constrain each other: a range's
start can't be after its end. Each such constraint is a named rule, as on
an object type you declare
([Rules across fields](objects.md#rules-across-fields)). So a
value you build in code is proved or attempted exactly as a value of your
own type is. Here are each type's rules, in the order they are checked,
which is also the order OpenAPI's description lists them in:

| Type | Rule | What it holds |
|---|---|---|
| `InstantRange`, `DateRange`, `Range<T>` | `start_not_after_end` | `start <= end`; an open end of a `Range<T>` keeps it |
| `IsoWeek` | `week_in_year` | week 53 only in a year with 53 ISO weeks |
| `CalendarEvent` | `days_or_times` | both days and no start, end or zone, or all three and no days |
| | `last_day_not_before_first` | `last_day >= first_day` |
| | `end_after_start` | `end > start` |
| | `recurrence_from_start` | the recurrence starts on `first_day`, or at `start` in `zone` |
| `PostalAddress` | `postal_code_of_country` | `postal_code`, where it has one, in its `country`'s form |
| `IdentityDocument` | `number_of_kind_and_country` | `number` in the form of its `kind` from its `country` |
| `PassportNumber`, `DrivingLicenceNumber`, `TaxId`, `VatNumber` | `number_of_country` | `number` in its `country`'s form |
| `ContentRating` | `rating_of_scheme` | `rating` one of its `scheme`'s categories |
| `Timecode` | `frames_within_rate` | `frames` below the rate's whole frames a second |
| | `drop_frame_rate` | drop-frame only at 30000/1001 and 60000/1001 |
| | `frame_not_dropped` | never a label drop-frame counting skips |
| `SubtitleCue` | `start_not_negative` | `start >= 0ms` |
| | `end_after_start` | `end > start` |
| | `text_without_arrow` | `text` holds no `-->` |
| `Rating` | `best_above_worst` | `best > worst` |
| | `value_on_scale` | `worst <= value and value <= best` |
| `BloodPressure` | `diastolic_below_systolic` | `diastolic < systolic` |
| `MapTile` | `within_zoom` | `x` and `y` each below 2 to the `zoom` |
| `TokenUsage` | `cached_within_input` | `cached <= input`, where it is given |
| `PasswordPolicy` | `min_not_above_max` | `min_length <= max_length` |
| `BoundingBox` | `south_not_above_north` | `south <= north` |
| `Polygon` | `rings_valid` | each ring closed and simple, no rings touching, holes inside the outer ring, every edge under half the globe ([Polygons are valid when built](predicates-and-shapes.md#polygons-are-valid-when-built)) |
| | `altitudes_agree` | every position with an altitude, or none ([Altitudes are all or nothing](predicates-and-shapes.md#altitudes-are-all-or-nothing)) |
| `LineString`, `MultiPoint`, `MultiLineString`, `MultiPolygon` | `altitudes_agree` | every position with an altitude, or none, a multi-geometry's parts with each other ([Altitudes are all or nothing](predicates-and-shapes.md#altitudes-are-all-or-nothing)) |

Here a slot built from two bookings' instants is attempted by its rule,
and one whose order a condition has already checked needs no `attempt`:

```jadpo
type SlotBackwards = Failure {
    kind: FailureKind.invalid_value
}

/// A slot from one booking's start to another's end.
function combined_slot(
    earlier_booking: InstantRange,
    later_booking: InstantRange
) fails SlotBackwards -> InstantRange {
    return attempt InstantRange {
        start: earlier_booking.start
        end: later_booking.end
    } {
        start_not_after_end: SlotBackwards
    }
}

/// The same, refused before it is built.
function combined_slot_checked(
    earlier_booking: InstantRange,
    later_booking: InstantRange
) fails SlotBackwards -> InstantRange {
    if (later_booking.end < earlier_booking.start) {
        reject SlotBackwards
    }
    return InstantRange {  // the condition proves `start_not_after_end`
        start: earlier_booking.start
        end: later_booking.end
    }
}
```

```jadpo
test "a slot never ends before it starts" {
    var morning_slot: InstantRange = InstantRange {
        start: "2026-10-26T09:00:00Z"
        end: "2026-10-26T12:00:00Z"
    }
    var afternoon_slot: InstantRange = InstantRange {
        start: "2026-10-26T13:00:00Z"
        end: "2026-10-26T17:00:00Z"
    }
    var whole_day: InstantRange = attempt combined_slot(morning_slot, afternoon_slot)
    assert whole_day.end == afternoon_slot.end
    assert combined_slot(afternoon_slot, morning_slot) fails SlotBackwards
    assert combined_slot_checked(afternoon_slot, morning_slot) fails SlotBackwards
}
```

- **Each rule behaves like a condition you write.** It is named, proved,
  attempted and reported exactly as a condition is. It runs on every
  value made, however it is made: an object literal, `text.parse`, a
  request's JSON or `json.read`, in compiled code as in the interpreter.
  The checker reads a literal by the same check. Every check across a
  standard type's parts is one of these rules, so there is no second
  mechanism.
- **How the checker proves a rule.** It reads a literal by what it shows
  of each part. A part written out is known, a part written `none` is
  absent, and a part built from a value of a type that can't be `none` is
  given. A rule its parts decide either holds or is the error
  `type.invalid_literal`, naming the rule.
- **A rule ordering two parts** (a range's ends, a rating's, a password
  policy's lengths) can otherwise be proved as a condition comparing two
  fields is: by a comparison known where the value is built, or by the
  declared ranges. A password policy's minimum length of 8, for example,
  is never above its maximum, which is at least 64. Any other rule on a
  value built from other values is attempted.
- **A rule may read one part.** A subtitle cue's start at 0 or later, and
  its text without `-->`, are named rules checked in the cue's order, not
  bounds the part's value would be built against on its own. Where one
  statement covers it, it is one rule: a rating's value from its worst to
  its best is `value_on_scale`, and a map tile's column and row below 2 to
  the power of its zoom is `within_zoom`.
- **Text and JSON break rules by name.** Text in the type's form that
  breaks a rule (week 53 of 2027, a cue ending before it starts, `PEGI
  12A`) is read and then breaks the rule; it isn't `malformed`. Only text
  that isn't the type at all is `malformed`. So
  `attempt text.parse(raw, as: T)` maps each of `T`'s rules, or
  `invalid:`. JSON reports the rule at the object's path, or at the
  field's path for a type sent as its text.
- **Text written where such a type goes is refused.** That text would be
  the type's canonical text, which is read only when the program runs.
  The checker doesn't have the readers the program uses for it, so it
  couldn't check the rules. The literal is `type.invalid_literal`: write
  the value out as an object literal, whose rules are checked, or read the
  text with `text.parse` and `attempt`. A type with one text form and no
  such rules (`SemanticVersion`, `CardExpiry`, `Colour`) still takes its
  text.
- **Any other literal where a standard object type goes is
  `type.mismatch`:** a number, a `Bool`, or text where a type with no
  single text form goes (`PostalAddress`, or WKT where a geometry goes,
  mixed altitudes included).
- **A value read from the store is checked against its rules,** as a
  value made anywhere else is. Jadpo never writes a value that breaks
  one, so a row that does was written by something else. Reading it is a
  defect naming the rule, never the values. A rule the running program
  can't decide, such as one on a part of the wrong kind, counts as broken.
- **OpenAPI** describes a standard object's schema with "Rules across its
  parts:" and each rule. `Range<T>` is an object of `start` and `end`,
  each its item or `null`, with its rule in its description.

A sale's dates written as a `DateRange`'s text can't be checked while
compiling:

```jadpo
/// When the shop's winter sale runs.
function winter_sale() -> DateRange {
    return "2026-12-27/2027-01-03"
}
```

```text
error: `"2026-12-27/2027-01-03"` can't be checked as a `DateRange` while compiling
 --> main.jadpo:3:12
  |
3 |     return "2026-12-27/2027-01-03"
  |            ^^^^^^^^^^^^^^^^^^^^^^^ canonical text
  reason: A `DateRange` has rules across its parts, which every literal is checked against while compiling; text written where one goes is its canonical text, which isn't read until the program runs, so its rules can't be checked.
  next: Write it out as `DateRange { ... }`, or read the text with `text.parse(..., as: DateRange)` and `attempt` (agent)
  rule: type.invalid_literal
  help: https://jadpo.dev/docs/errors/type.invalid_literal

check failed: 1 error, 1 file
```

Written out, the sale's rules are checked while compiling:

```jadpo
/// When the shop's winter sale runs.
function winter_sale() -> DateRange {
    return DateRange {
        start: "2026-12-27"
        end: "2027-01-03"
    }
}
```
