form.date-parts
Check a date typed into day, month and year boxes, with GOV.UK's error messages and the boxes to mark in error.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 49 tests, run in TypeScript, Python and Rust.
What it does
Checks a date someone typed into three boxes - day, month, year - the way the GOV.UK Design System's date input asks, and answers with GOV.UK's own error message and which boxes to mark in error. The same function runs in the browser (TypeScript), in a Python API and in a Rust service, so the page and the server never disagree about a date or word the error differently.
import { validateDateParts } from "#fune/form.date-parts@^1";
const check = validateDateParts(
{ day: form.dobDay, month: form.dobMonth, year: form.dobYear },
"Date of birth",
{ today: "2026-09-28", timing: "past", notBefore: null, notAfter: null },
);
// { valid: false, date: null, message: "Date of birth must be in the past", fields: ["day", "month", "year"] }
For example
validate_date_parts(day 27, month 3, year 2007, Date of birth, today —, timing —, not before —, not after —)→ valid true, date 2007-03-27, message —, fields a whole date, GOV.UK's example 27 3 2007validate_date_parts(day 27 , month 3 , year 2007 , Date of birth, today —, timing —, not before —, not after —)→ valid true, date 2007-03-27, message —, fields spaces and a trailing newline are trimmed from every boxvalidate_date_parts(day 02, month 2, year 2024, Date of birth, today —, timing —, not before —, not after —)→ valid true, date 2024-02-02, message —, fields a leading zero or none: 02 and 2 are both February
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn validate_date_parts(parts: &DateParts, label: &str, rules: &DateRules) -> DatePartsCheck
| parts | DateParts | the three boxes as typed; each is trimmed, and the month may be a name (jan, January) |
| label | string | what the date is, as it starts a sentence: "Date of birth", "The date your course ends" |
| rules | DateRules | optional past/future and earliest/latest checks; today is passed in, never read from the clock |
| returns | DatePartsCheck | valid with the ISO date, or GOV.UK's message and which of day, month and year to mark |
The types it declares, generated into your project
/// What a person typed into a date input's three boxes, untrimmed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DateParts {
pub day: String,
pub month: String,
pub year: String,
}
// DateTiming is a string in Rust, one of: "any", "past", "past-or-today", "future", "future-or-today".
// Parameters take it as &str and results hold it as String.
/// Checks on a real date beyond its being real; null everywhere means none.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DateRules {
/// needed when timing is not any or null
pub today: Option<String>,
/// past, past-or-today, future or future-or-today, relative to today
pub timing: Option<String>,
/// the earliest date allowed, inclusive
pub not_before: Option<String>,
/// the latest date allowed, inclusive
pub not_after: Option<String>,
}
/// The verdict on a typed date, shaped for a date input's error message and error classes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatePartsCheck {
pub valid: bool,
/// the ISO date, when valid
pub date: Option<String>,
/// the error message to show, when not valid
pub message: Option<String>,
/// which of "day", "month" and "year" to mark in error, in that order; empty when valid
pub fields: Vec<String>,
}
Your code names it in one line, in the file that uses it
fune!(form.date-parts@^1); // then call validate_date_parts(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{days_in_month, parse_iso_date}; ← from dates.add-days ^1.0.0 · built alongside by fune
const MONTH_NAMES: [&str; 12] = [
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November",
"December",
];
const PARTS: [&str; 3] = ["day", "month", "year"];
// ASCII whitespace only, so all three languages trim the same characters
// (str::trim would also take Unicode spaces).
fn trim_ascii(value: &str) -> &str {
value.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0c' | '\x0b'))
}
// ASCII digits only: "٢" is not a day in any of the three languages.
fn is_digits(value: &str) -> bool {
!value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
}
// A day or month number: one or two digits.
fn small_number(value: &str, largest: i64) -> Option<i64> {
if !is_digits(value) || value.len() > 2 {
return None;
}
let n: i64 = value.parse().ok()?;
if (1..=largest).contains(&n) {
Some(n)
} else {
None
}
}
// GOV.UK asks services to accept month names in full or short ("jan"), in
// any case; "sept" is common enough to accept too.
fn month_number(value: &str) -> Option<i64> {
if let Some(n) = small_number(value, 12) {
return Some(n);
}
let lower = value.to_ascii_lowercase();
if lower == "sept" {
return Some(9);
}
for (i, full) in MONTH_NAMES.iter().enumerate() {
let name = full.to_ascii_lowercase();
if lower == name || lower == name[..3] {
return Some(i as i64 + 1);
}
}
None
}
// "Date of birth" starts a sentence; after "Enter" it is "date of birth".
// A label that starts with an acronym ("UK arrival date") is left alone.
fn lower_first(label: &str) -> String {
let bytes = label.as_bytes();
let upper = |i: usize| i < bytes.len() && bytes[i].is_ascii_uppercase();
if upper(0) && !upper(1) {
let mut out = String::with_capacity(label.len());
out.push(bytes[0].to_ascii_lowercase() as char);
out.push_str(&label[1..]);
out
} else {
label.to_string()
}
}
fn long_date(iso: &str) -> String {
let d = parse_iso_date(iso);
format!("{} {} {}", d.day, MONTH_NAMES[(d.month - 1) as usize], d.year)
}
fn invalid(message: String, fields: &[&str]) -> DatePartsCheck {
DatePartsCheck {
valid: false,
date: None,
message: Some(message),
fields: fields.iter().map(|f| f.to_string()).collect(),
}
}
/// A date typed into three boxes, checked the way the GOV.UK Design System's
/// date input asks: missing or incomplete information first, then a date that
/// cannot be real, then the service's own rules, each with GOV.UK's wording.
///
/// # Panics
/// Panics on an empty label, a timing rule without today, a malformed rule
/// date, or notBefore after notAfter.
pub fn validate_date_parts(parts: &DateParts, label: &str, rules: &DateRules) -> DatePartsCheck {
let name = trim_ascii(label);
if name.is_empty() {
panic!("a date needs a label, such as \"Date of birth\", to write its error messages");
}
// Rules are the caller's, so a bad one fails loudly whatever was typed.
let timing = rules.timing.as_deref().unwrap_or("any");
if timing != "any" && rules.today.is_none() {
panic!("rules.today is needed to check that a date is {}", timing.replace('-', " "));
}
for bound in [&rules.today, &rules.not_before, &rules.not_after].into_iter().flatten() {
parse_iso_date(bound);
}
if let (Some(before), Some(after)) = (&rules.not_before, &rules.not_after) {
if before > after {
panic!("rules.notBefore ({}) must not be after rules.notAfter ({})", before, after);
}
}
let typed = [trim_ascii(&parts.day), trim_ascii(&parts.month), trim_ascii(&parts.year)];
let year = typed[2];
let missing: Vec<&str> = (0..3).filter(|&i| typed[i].is_empty()).map(|i| PARTS[i]).collect();
if missing.len() == 3 {
return invalid(format!("Enter {}", lower_first(name)), &PARTS);
}
if !missing.is_empty() {
let what = if missing.len() == 1 { missing[0].to_string() } else { format!("{} and {}", missing[0], missing[1]) };
return invalid(format!("{} must include a {}", name, what), &missing);
}
if is_digits(year) && year.len() != 4 {
return invalid("Year must include 4 numbers".to_string(), &["year"]);
}
let d = small_number(typed[0], 31);
let m = month_number(typed[1]);
let y = if is_digits(year) { year.parse::<i64>().ok().filter(|&n| n >= 1) } else { None };
let found = [d, m, y];
let wrong: Vec<&str> = (0..3).filter(|&i| found[i].is_none()).map(|i| PARTS[i]).collect();
if !wrong.is_empty() {
let fields: &[&str] = if wrong.len() == 1 { &wrong } else { &PARTS };
return invalid(format!("{} must be a real date", name), fields);
}
let (d, m, y) = (d.unwrap(), m.unwrap(), y.unwrap());
// 31 April or 29 February 2023: the day is what does not exist.
if d > days_in_month(y, m) {
return invalid(format!("{} must be a real date", name), &["day"]);
}
let iso = format!("{}-{:02}-{:02}", year, m, d);
let iso = iso.as_str();
let today = rules.today.as_deref().unwrap_or("");
if timing == "past" && iso >= today {
return invalid(format!("{} must be in the past", name), &PARTS);
}
if timing == "past-or-today" && iso > today {
return invalid(format!("{} must be today or in the past", name), &PARTS);
}
if timing == "future" && iso <= today {
return invalid(format!("{} must be in the future", name), &PARTS);
}
if timing == "future-or-today" && iso < today {
return invalid(format!("{} must be today or in the future", name), &PARTS);
}
match (rules.not_before.as_deref(), rules.not_after.as_deref()) {
(Some(before), Some(after)) => {
if iso < before || iso > after {
return invalid(
format!("{} must be between {} and {}", name, long_date(before), long_date(after)),
&PARTS,
);
}
}
(Some(before), None) if iso < before => {
return invalid(format!("{} must be the same as or after {}", name, long_date(before)), &PARTS);
}
(None, Some(after)) if iso > after => {
return invalid(format!("{} must be the same as or before {}", name, long_date(after)), &PARTS);
}
_ => {}
}
DatePartsCheck { valid: true, date: Some(iso.to_string()), message: None, fields: Vec::new() }
}
pub fn date_parts_check_to_value(check: &DatePartsCheck) -> Value {
let text = |v: &Option<String>| v.as_deref().map(Value::str).unwrap_or(Value::Null);
Value::obj(vec![
("valid", Value::Bool(check.valid)),
("date", text(&check.date)),
("message", text(&check.message)),
("fields", Value::Arr(check.fields.iter().map(|f| Value::str(f)).collect())),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let text = |v: &Value, key: &str| match v.get(key) {
Value::Str(s) => s.clone(),
_ => panic!("parts.{} must be a string", key),
};
let optional = |v: &Value, key: &str| match v.get(key) {
Value::Str(s) => Some(s.clone()),
_ => None,
};
let parts = DateParts { day: text(&args[0], "day"), month: text(&args[0], "month"), year: text(&args[0], "year") };
let rules = DateRules {
today: optional(&args[2], "today"),
timing: optional(&args[2], "timing"),
not_before: optional(&args[2], "notBefore"),
not_after: optional(&args[2], "notAfter"),
};
date_parts_check_to_value(&validate_date_parts(&parts, args[1].as_str(), &rules))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add form.date-parts
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./form.date-parts-1.0.0-rust.fune, or fetch it from a terminal with fune pull form.date-parts@1.0.0:rust.
The whole function, every language, is one file too: form.date-parts-1.0.0.fune, 46,177 bytes, sha256 7f230eb3a77811cc7a49386345b5940ccd413e70aa365a65e67d2c221e01b958. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before form.date-parts
after — your function gets the result and the arguments, and returns the final result.
// fune: after form.date-parts
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace dates.add-days in form.date-parts
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show form.date-parts --steps.
// fune: step form.date-parts after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a whole date, GOV.UK's example 27 3 2007 | day 27, month 3, year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2007-03-27, message —, fields |
| spaces and a trailing newline are trimmed from every box | day 27 , month 3 , year 2007 , Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2007-03-27, message —, fields |
| a leading zero or none: 02 and 2 are both February | day 02, month 2, year 2024, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2024-02-02, message —, fields |
| 29 February in a leap year | day 29, month 2, year 2024, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2024-02-29, message —, fields |
| 29 February 2023 does not exist: the day is marked | day 29, month 2, year 2023, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day |
| 1900 was not a leap year, though 1900 % 4 is 0 | day 29, month 02, year 1900, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day |
| 2000 was a leap year | day 29, month 2, year 2000, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2000-02-29, message —, fields |
| 31 April does not exist | day 31, month 4, year 2026, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day |
| 31 December is the last day | day 31, month 12, year 1999, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 1999-12-31, message —, fields |
| nothing entered: Enter, with the label in lower case, and the whole date marked | day , month , year , Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Enter date of birth, fields day, month, year |
Show the other 39 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a missing month | day 27, month , year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must include a month, fields month |
| a missing day and year | day , month 3, year , Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must include a day and year, fields day, year |
| a missing day and month, GOV.UK's example | day , month , year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must include a day and month, fields day, month |
| a two-digit year is incomplete | day 27, month 3, year 07, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Year must include 4 numbers, fields year |
| a five-digit year is not 4 numbers either | day 27, month 3, year 20077, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Year must include 4 numbers, fields year |
| missing information comes before an incomplete year | day , month 3, year 07, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must include a day, fields day |
| an incomplete year comes before an impossible day | day 32, month 3, year 07, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Year must include 4 numbers, fields year |
| month 13 cannot be correct | day 27, month 13, year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields month |
| day 0 cannot be correct | day 0, month 3, year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day |
| two wrong boxes: the whole date is marked | day 32, month 13, year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day, month, year |
| letters in the year | day 27, month 3, year 20o7, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields year |
| year 0000 does not exist | day 1, month 1, year 0000, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields year |
| three digits in the day | day 027, month 3, year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day |
| an Arabic-Indic digit is not a day in any of the languages | day ٢, month 3, year 2007, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields day |
| a month name in full, any case | day 5, month January, year 2026, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2026-01-05, message —, fields |
| a short month name | day 5, month jan, year 2026, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2026-01-05, message —, fields |
| SEPT is September | day 9, month SEPT, year 2026, Date of birth, today —, timing —, not before —, not after — | → | valid true, date 2026-09-09, message —, fields |
| a month name cut at four letters is not one | day 5, month Janu, year 2026, Date of birth, today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must be a real date, fields month |
| in the past: today is not in the past | day 28, month 9, year 2026, Date of birth, today 2026-09-28, timing past, not before —, not after — | → | valid false, date —, message Date of birth must be in the past, fields day, month, year |
| in the past: yesterday is | day 27, month 9, year 2026, Date of birth, today 2026-09-28, timing past, not before —, not after — | → | valid true, date 2026-09-27, message —, fields |
| today or in the past: today is allowed | day 28, month 9, year 2026, Date of birth, today 2026-09-28, timing past-or-today, not before —, not after — | → | valid true, date 2026-09-28, message —, fields |
| today or in the past: tomorrow is not | day 29, month 9, year 2026, Date of birth, today 2026-09-28, timing past-or-today, not before —, not after — | → | valid false, date —, message Date of birth must be today or in the past, fields day, month, year |
| in the future, GOV.UK's example label | day 28, month 9, year 2026, The date your course ends, today 2026-09-28, timing future, not before —, not after — | → | valid false, date —, message The date your course ends must be in the future, fields day, month, year |
| today or in the future: yesterday is not | day 27, month 9, year 2026, The date your course ends, today 2026-09-28, timing future-or-today, not before —, not after — | → | valid false, date —, message The date your course ends must be today or in the future, fields day, month, year |
| today or in the future: today is | day 28, month 9, year 2026, The date your course ends, today 2026-09-28, timing future-or-today, not before —, not after — | → | valid true, date 2026-09-28, message —, fields |
| timing any ignores today | day 1, month 1, year 2099, Date of birth, today 2026-09-28, timing any, not before —, not after — | → | valid true, date 2099-01-01, message —, fields |
| between two dates, GOV.UK's example | day 1, month 10, year 2017, The date your contract started, today —, timing —, not before 2017-09-01, not after 2017-09-30 | → | valid false, date —, message The date your contract started must be between 1 September 2017 and 30 September 2017, fields day, month, year |
| between is inclusive at both ends | day 30, month 9, year 2017, The date your contract started, today —, timing —, not before 2017-09-01, not after 2017-09-30 | → | valid true, date 2017-09-30, message —, fields |
| not before a date | day 31, month 8, year 2017, The date your course ends, today —, timing —, not before 2017-09-01, not after — | → | valid false, date —, message The date your course ends must be the same as or after 1 September 2017, fields day, month, year |
| not after a date | day 1, month 9, year 2017, The date of the last exam, today —, timing —, not before —, not after 2017-08-31 | → | valid false, date —, message The date of the last exam must be the same as or before 31 August 2017, fields day, month, year |
| a bound on the day itself is allowed | day 31, month 8, year 2017, The date of the last exam, today —, timing —, not before —, not after 2017-08-31 | → | valid true, date 2017-08-31, message —, fields |
| a real-date problem comes before a rule | day 31, month 4, year 2017, The date of the last exam, today —, timing —, not before —, not after 2017-08-31 | → | valid false, date —, message The date of the last exam must be a real date, fields day |
| a label that starts with an acronym keeps its capitals after Enter | day , month , year , UK arrival date, today —, timing —, not before —, not after — | → | valid false, date —, message Enter UK arrival date, fields day, month, year |
| the label is trimmed | day , month 3, year 2007, Date of birth , today —, timing —, not before —, not after — | → | valid false, date —, message Date of birth must include a day, fields day |
| a timing rule needs today | day 1, month 1, year 2000, Date of birth, today —, timing past, not before —, not after — | → | error: rules.today is needed to check that a date is past |
| a malformed bound is refused | day 1, month 1, year 2000, Date of birth, today —, timing —, not before 2017-9-1, not after — | → | error: "2017-9-1" is not an ISO date |
| a bound that cannot exist is refused | day 1, month 1, year 2000, Date of birth, today —, timing —, not before —, not after 2017-02-30 | → | error: "2017-02-30" is not a real calendar date |
| notBefore after notAfter is refused | day 1, month 1, year 2000, Date of birth, today —, timing —, not before 2018-01-01, not after 2017-01-01 | → | error: rules.notBefore (2018-01-01) must not be after rules.notAfter (2017-01-01) |
| an empty label is refused | day 1, month 1, year 2000, , today —, timing —, not before —, not after — | → | error: a date needs a label |
More from the author
check = validate_date_parts(DateParts(day="31", month="4", year="2026"), "Date of birth", DateRules(today=None, timing=None, not_before=None, not_after=None))
# DatePartsCheck(valid=False, date=None, message="Date of birth must be a real date", fields=["day"])`message` goes to `react.form.date-input`'s `error` and `fields` to its `errorFields`, which marks exactly those boxes.
## The checks, in GOV.UK's order of priority
GOV.UK shows the highest-priority error only: missing or incomplete information, then information that cannot be correct, then anything else.
| What was typed | Message | Marked | | --- | --- | --- | | nothing in any box | `Enter date of birth` | all three | | some boxes empty | `Date of birth must include a month`, `... a day and year` | the empty ones | | a year of digits that are not 4 | `Year must include 4 numbers` | year | | a box that cannot be right (day 32, month 13, letters, year 0000) | `Date of birth must be a real date` | that box, or all three when more than one is wrong | | a day the month does not have (31 April, 29 February 2023) | `Date of birth must be a real date` | day | | `timing` past / past-or-today / future / future-or-today | `... must be in the past`, `... must be today or in the past`, `... must be in the future`, `... must be today or in the future` | all three | | `notBefore` and `notAfter` | `... must be between 1 September 2017 and 30 September 2017` | all three | | `notBefore` alone | `... must be the same as or after 1 September 2017` | all three | | `notAfter` alone | `... must be the same as or before 31 August 2017` | all three |
Otherwise it answers `{ valid: true, date: "2007-03-27", message: null, fields: [] }`.
- **The label** is written as it starts a sentence ("Date of birth", "The date your course ends"). After "Enter" its first letter is lower-cased ("Enter date of birth"), unless it starts with an acronym ("Enter UK arrival date"): two capitals in a row are left alone. GOV.UK's own example reads "Enter your date of birth"; pass "Your date of birth" as the label for that, and every other message then starts "Your date of birth must...". - **"Year must include 4 numbers"** is GOV.UK's wording verbatim and does not use the label. - **Trimming**: each box loses leading and trailing ASCII whitespace (space, tab, line breaks), the same in all three languages. - **Numbers** are ASCII digits only: `"٢"` is not a day. A day or month is one or two digits, so `"2"` and `"02"` are both accepted and `"002"` is not. - **Month names** are accepted, as GOV.UK asks, in full or as their first three letters, in any case (`January`, `jan`, `JAN`), plus `sept`. English only. - **Dates are proleptic Gregorian**, years 1 to 9999, the range of the `dates.*` family; the leap-year rule and month lengths come from `dates.add-days`, not a copy. - **Which box for 31 April**: GOV.UK says to mark the box that is wrong, or the whole date when it is not clear. This marks the day, which is where the fix almost always is. - **Today is an argument.** Nothing reads the clock, so the check is the same in a test, on a server in another timezone, and in the browser. Pass the date that "today" means for your service (usually the UK date). - Rules are checked before the date: a timing without `today`, a bound that is not an ISO date, or `notBefore` after `notAfter` throws whatever was typed, since that is a bug in the caller, not a mistake by the user. So does an empty label.
## Sources
GOV.UK Design System, "Date input", error messages and month names: https://design-system.service.gov.uk/components/date-input/ and the "Dates" pattern https://design-system.service.gov.uk/patterns/dates/. The messages above were checked word for word against the "Error messages" section of the component page.
Files
| Path | Bytes |
|---|---|
| README.md | 4,621 |
| impl/python.py | 5,906 |
| impl/rust.rs | 7,848 |
| impl/typescript.ts | 5,891 |
| vectors.json | 14,750 |