Validation

Postal codes

Postal codes checked for 45 countries and written the post office's way: SW1A 1AA, 1012 JS, K1A 0B1, 100-0001, 20500-0001.

Version 1.0.0 · 4 functions · 1 crate

#addresses#forms#validation#shipping

Functions

What use postal_codes.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use postal_valid, postal_format from postal_codes (Quillo 0.6).

postal_valid(code: str, country: str) → boolor an error

Whether code is a postal code of country (alpha-2, "US", "GB"...), spaces, dashes and case ignored. It checks the form, not that the code is in use.

postal_format(code: str, country: str) → stror an error

code written as the country's post office writes it ("sw1a1aa" is "SW1A 1AA" in GB, "12345-6789" stays so in the US), or an error saying it is not a postal code there.

postal_check(code: str, country: str) → anyor an error

What a form needs for one country: { valid, formatted, example }, formatted null when it is not valid. An error for a country without rules.

postal_countries() → list

The countries with rules, as alpha-2 codes.

Get it

$ quillo install postal_codes               # postal_codes.qwer into this folder$ quillo install postal_codes --example     # and its example, postal_codes.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/postal_codes.qwer$ curl -O https://quillolang.org/plugins/files/postal_codes.qwe

Then, in your program, all of it or only what you call:

main.qwe
use postal_codes.qweruse postal_valid, postal_format from postal_codes      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
regex = { version = "1", default-features = false, features = ["std", "unicode-perl"] }

Example

postal_codes.qwe
// An address form that checks the postal code for the chosen country and writes it the post office's way.////   quillo run postal_codes.qwe      then open http://localhost:8080/check?code=sw1a1aa&country=GBuse "postal_codes.qwer"route GET /check ? { code: str, country: str } = postal_check(code, country)?route GET /examples = [    { code: "sw1a1aa", country: "GB" }, { code: "1012js", country: "NL" }, { code: "k1a0b1", country: "CA" },    { code: "1000001", country: "JP" }, { code: "205000001", country: "US" }, { code: "00950", country: "PL" },    { code: "1050", country: "LV" }, { code: "1234", country: "DE" },] | map((a) => { country: a.country, typed: a.code, shown: postal_format(a.code, a.country) ?? "invalid" })route POST /address {    let a = request.json ?? {}    if !postal_valid(a.postal_code ?? "", a.country ?? "")? { reply 422 { error: "Please check the postal code." } }    { ok: true, postal_code: postal_format(a.postal_code, a.country)? }}
$ quillo run postal_codes.qwe$ curl 'localhost:8080/check?code=sw1a1aa&country=GB'$ curl 'localhost:8080/examples'

Source

postal_codes.qwer
//! Postal codes checked and written the way each country's post office writes them, for 45 countries: "sw1a1aa" is "SW1A 1AA" in the UK, "1234ab" is "1234 AB" in the Netherlands, "1000001" is "100-0001" in Japan.//! version: 1.0.0//!//! [dependencies]//! regex = { version = "1", default-features = false, features = ["std", "unicode-perl"] }use regex::Regex;use serde_json::{json, Value};use std::collections::HashMap;use std::sync::OnceLock;/// Each country: the code with spaces and dashes taken out, as a pattern; how it is written (`#` is the next character of the code; a pattern per length); an example.const RULES: &[(&str, &str, &[&str], &str)] = &[    ("AR", r"[A-Z]\d{4}[A-Z]{3}|\d{4}", &["#", "#"], "C1425DKF"),    ("AT", r"[1-9]\d{3}", &["####"], "1010"),    ("AU", r"\d{4}", &["####"], "2000"),    ("BE", r"[1-9]\d{3}", &["####"], "1000"),    ("BG", r"\d{4}", &["####"], "1000"),    ("BR", r"\d{8}", &["#####-###"], "01310-100"),    ("CA", r"[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z]\d[ABCEGHJ-NPRSTV-Z]\d", &["### ###"], "K1A 0B1"),    ("CH", r"[1-9]\d{3}", &["####"], "8001"),    ("CN", r"\d{6}", &["######"], "100000"),    ("CZ", r"[1-7]\d{4}", &["### ##"], "110 00"),    ("DE", r"\d{5}", &["#####"], "10115"),    ("DK", r"[1-9]\d{3}", &["####"], "1050"),    ("EE", r"\d{5}", &["#####"], "10111"),    ("ES", r"(0[1-9]|[1-4]\d|5[0-2])\d{3}", &["#####"], "28013"),    ("FI", r"\d{5}", &["#####"], "00100"),    ("FR", r"\d{5}", &["#####"], "75008"),    ("GB", r"GIR0AA|[A-PR-UWYZ](\d{1,2}|[A-HK-Y]\d{1,2}|\d[A-HJKPS-UW]|[A-HK-Y]\d[ABEHMNPRV-Y])\d[ABD-HJLNP-UW-Z]{2}", &["@ ###"], "SW1A 1AA"),    ("GR", r"[1-8]\d{4}", &["### ##"], "105 57"),    ("HR", r"[1-5]\d{4}", &["#####"], "10000"),    ("HU", r"[1-9]\d{3}", &["####"], "1051"),    ("IE", r"([AC-FHKNPRTV-Y]\d{2}|D6W)[0-9AC-FHKNPRTV-Y]{4}", &["### ####"], "D02 X285"),    ("IL", r"\d{7}", &["#######"], "6100000"),    ("IN", r"[1-9]\d{5}", &["######"], "110001"),    ("IS", r"\d{3}", &["###"], "101"),    ("IT", r"\d{5}", &["#####"], "00184"),    ("JP", r"\d{7}", &["###-####"], "100-0001"),    ("KR", r"\d{5}", &["#####"], "03187"),    ("LT", r"(LT)?\d{5}", &["LT-#####"], "LT-01100"),    ("LU", r"\d{4}", &["####"], "1009"),    ("LV", r"(LV)?\d{4}", &["LV-####"], "LV-1050"),    ("MX", r"\d{5}", &["#####"], "06600"),    ("NL", r"[1-9]\d{3}(?:[A-RT-Z][A-Z]|S[BCE-RT-Z])", &["#### ##"], "1012 JS"),    ("NO", r"\d{4}", &["####"], "0150"),    ("NZ", r"\d{4}", &["####"], "6011"),    ("PL", r"\d{5}", &["##-###"], "00-950"),    ("PT", r"[1-9]\d{6}", &["####-###"], "1100-148"),    ("RO", r"\d{6}", &["######"], "010011"),    ("RU", r"\d{6}", &["######"], "101000"),    ("SE", r"[1-9]\d{4}", &["### ##"], "111 22"),    ("SG", r"\d{6}", &["######"], "018956"),    ("SI", r"[1-9]\d{3}", &["####"], "1000"),    ("SK", r"[089]\d{4}", &["### ##"], "811 01"),    ("TR", r"(0[1-9]|[1-7]\d|8[01])\d{3}", &["#####"], "34000"),    ("US", r"\d{5}(\d{4})?", &["#####", "#####-####"], "20500"),    ("ZA", r"\d{4}", &["####"], "8001"),];fn rules() -> &'static HashMap<&'static str, (Regex, &'static [&'static str], &'static str)> {    static R: OnceLock<HashMap<&str, (Regex, &[&str], &str)>> = OnceLock::new();    R.get_or_init(|| RULES.iter().filter_map(|(c, p, f, e)| Regex::new(&format!("^(?:{})$", p)).ok().map(|r| (*c, (r, *f, *e)))).collect())}fn compact(code: &str) -> String {    code.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_uppercase()}/// Writes the compact code by a layout: `#` is the next character, `@` the characters before the last three.fn layout(c: &str, f: &str) -> String {    let c = c.trim_start_matches("LT").trim_start_matches("LV");    if f == "#" {        return c.to_string();    }    let mut out = String::new();    let mut it = c.chars();    for p in f.chars() {        match p {            '#' => out.extend(it.next()),            '@' => out.extend(it.by_ref().take(c.len().saturating_sub(3))),            other => out.push(other),        }    }    out.extend(it);    out}fn get(country: &str) -> Result<&'static (Regex, &'static [&'static str], &'static str), String> {    let cc = country.trim().to_uppercase();    let cc = if cc == "UK" { "GB".to_string() } else { cc };    rules().get(cc.as_str()).ok_or_else(|| format!("no postal code rules for {:?}; see postal_countries()", country))}/// Whether `code` is a postal code of `country` (alpha-2, "US", "GB"...), spaces, dashes and case ignored. It checks the form, not that the code is in use.pub fn postal_valid(code: &str, country: &str) -> Result<bool, String> {    let c = compact(code);    Ok(c.len() <= 12 && get(country)?.0.is_match(&c))}/// `code` written as the country's post office writes it ("sw1a1aa" is "SW1A 1AA" in GB, "12345-6789" stays so in the US), or an error saying it is not a postal code there.pub fn postal_format(code: &str, country: &str) -> Result<String, String> {    let c = compact(code);    let (re, formats, example) = get(country)?;    if c.len() > 12 || !re.is_match(&c) {        return Err(format!("{:?} is not a postal code in {}: they look like {}", code, country.to_uppercase(), example));    }    let bare = c.trim_start_matches("LT").trim_start_matches("LV");    // The US has two layouts, by length; others one.    let f = if formats.len() > 1 && bare.len() > 5 { formats[1] } else { formats[0] };    Ok(layout(&c, f))}/// What a form needs for one country: `{ valid, formatted, example }`, `formatted` null when it is not valid. An error for a country without rules.pub fn postal_check(code: &str, country: &str) -> Result<Value, String> {    let (_, _, example) = get(country)?;    let f = postal_format(code, country).ok();    Ok(json!({ "valid": f.is_some(), "formatted": f, "example": example }))}/// The countries with rules, as alpha-2 codes.pub fn postal_countries() -> Vec<String> {    RULES.iter().map(|r| r.0.to_string()).collect()}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On Postal codes: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.