// demo.mbt — a runnable tour of the public API.
//
// This exists because the acceptance criteria ask for a runnable example, not
// just a README snippet: something a reviewer can execute end to end and compare
// against the recorded output in docs/reproducible-demo.md.
//
// Run it with:
// moon run cmd/demo
//
// Every value printed here comes from the library itself. Nothing is hard-coded
// to make the output look right, so if the library regresses this output changes.
///|
fn head(title : String) -> Unit {
println("")
println("== \{title} ==")
}
///|
fn show(label : String, value : String) -> Unit {
println("\{label}: \{value}")
}
///|
fn join(items : Array[String], sep : String) -> String {
let buf = StringBuilder()
for i = 0; i < items.length(); i = i + 1 {
if i > 0 {
buf.write_string(sep)
}
buf.write_string(items[i])
}
buf.to_string()
}
///|
/// Parse one written number and print the useful facts about it.
fn tour(input : String, region : String) -> Unit {
show(
"input",
input + (if region == "" { "" } else { " [region " + region + "]" }),
)
match @dn.parse(input, region) {
None => show("result", "unreadable")
Some(pn) => {
show("e164", @dn.format_e164(pn))
show("international", @dn.format_international(pn))
let reg = match @dn.region_of_number(pn) {
Some(c) => c
None => "unresolved"
}
show("region", reg)
show("possible", if @dn.is_possible(pn) { "yes" } else { "no" })
show("valid", if @dn.is_valid(pn) { "yes" } else { "no" })
match @dn.number_type(pn) {
Some(t) => show("type", t.to_string())
None => show("type", "(none)")
}
}
}
}
///|
/// The number types a length admits, as a comma-separated list.
fn types_of(input : String, region : String) -> Unit {
match @dn.parse(input, region) {
None => show("types", "unreadable")
Some(pn) => {
let names : Array[String] = []
for t in @dn.possible_types(pn) {
names.push(t.to_string())
}
show(input, join(names, ","))
}
}
}
///|
/// One line of region metadata drawn straight from the table.
fn fact(code : String) -> Unit {
let name = match @dn.region_name(code) {
Some(n) => n
None => "?"
}
let cc = match @dn.calling_code(code) {
Some(c) => c.to_string()
None => "?"
}
let lens : Array[String] = []
match @dn.possible_lengths(code) {
Some(ls) =>
for n in ls {
lens.push(n.to_string())
}
None => ()
}
show(code, name + " +" + cc + " lengths {" + join(lens, ",") + "}")
}
///|
fn main {
head("parse across the ways people write a number")
tour("+1 201-555-0123", "")
tour("011 44 20 7946 0958", "US")
tour("090-1234-5678", "JP")
tour("+86 (131) 2345-6789", "")
tour("02 12345678", "IT")
tour("+999 555 0000", "")
head("length is what separates possible from impossible")
tour("+1 201-555-012", "")
tour("+1 201-555-01234", "")
head("national rendering needs a region")
match @dn.parse("090-1234-5678", "JP") {
None => show("national", "unreadable")
Some(pn) => {
let nat = match @dn.format_national(pn, "JP") {
Some(v) => v
None => "(none)"
}
show("national", nat)
}
}
head("each plan groups the number its own way")
// The grouping is the plans' own: each publishes the shapes its numbers are
// written in, and the renderer picks the rule that fits.
let styled : Array[(String, String)] = [
("+1 201-555-0123", "US"),
("+44 20 7946 0958", "GB"),
("+49 30 123456", "DE"),
("+33 1 23 45 67 89", "FR"),
("+81 90-1234-5678", "JP"),
("+55 11 98765-4321", "BR"),
("+61 2 1234 5678", "AU"),
]
for pair in styled {
match @dn.parse(pair.0, "") {
None => show(pair.1, "unreadable")
Some(pn) => {
let nat = match @dn.format_national(pn, pair.1) {
Some(v) => v
None => "(none)"
}
show(pair.1, "national: " + nat + " international: " + @dn.format_international(pn))
}
}
}
head("+1 is 25 territories, and each allocation decides")
// Every +1 territory publishes its own area-code allocation; the engine
// resolves the number against it instead of a hard-coded US/CA split.
tour("+1 201-555-0123", "")
tour("+1 204-254-5000", "")
tour("+1 787-234-5678", "")
tour("+1 876-523-0123", "")
tour("+1 242-345-6789", "")
tour("+1 868-221-1234", "")
tour("+1 111-555-0123", "")
tour("+1 201-155-0123", "")
head("possible is a length question, valid is a rule question")
// 555 passes the NANP's shape rules but is not an assigned area code, so the
// number is possible and not valid — the distinction the two levels exist for.
tour("+1 555-555-0123", "")
tour("+1 201-555-0123", "")
tour("+81 90-1234-567", "")
head("the rules are data the engine interprets")
match @dn.plan_pattern("DK") {
Some(p) => show("DK plan pattern", p)
None => show("DK plan pattern", "(none)")
}
show("pattern matches 12345678", if @dn.matches_pattern("\\d{8}", "12345678") { "yes" } else { "no" })
show("pattern matches 1234567", if @dn.matches_pattern("\\d{8}", "1234567") { "yes" } else { "no" })
head("a parsed number keeps its extension")
match @dn.parse_tel_uri("tel:+1-201-555-0123;ext=42") {
None => show("tel uri", "unreadable")
Some(pn) => {
show("e164", @dn.format_e164(pn))
show("extension", pn.extension)
show("rfc3966", @dn.format_rfc3966(pn))
}
}
head("a tel: URI keeps its local context")
// A local number with a numeric context resolves; the context is preserved
// rather than merged away, so the canonical form can be regenerated.
match @dn.parse_tel_uri_full("tel:555-0123;phone-context=+1-919;ext=42") {
None => show("tel uri", "rejected")
Some(t) => {
show("local_number", t.local_number)
show("phone_context", t.phone_context)
show("canonical", t.to_string())
match @dn.resolve_tel_uri(t) {
Some(pn) => show("resolved", @dn.format_e164(pn))
None => show("resolved", "no")
}
}
}
// A domain context is valid RFC 3966 but cannot be resolved to a number here.
match @dn.parse_tel_uri_full("tel:7042;phone-context=example.com") {
None => show("tel uri", "rejected")
Some(t) => {
show("phone_context", t.phone_context)
show("canonical", t.to_string())
match @dn.resolve_tel_uri(t) {
Some(pn) => show("resolved", @dn.format_e164(pn))
None => show("resolved", "no (domain context)")
}
}
}
head("the number types a length admits")
types_of("+86 131 2345 6789", "")
types_of("+44 7400 123456", "")
head("dialling from where you are")
// The same number, as a caller in three countries would dial it.
match @dn.parse("+442079460958", "") {
None => show("dialling", "unreadable")
Some(pn) => {
for from in ["US", "DE", "GB", "AU"] {
let written = match @dn.format_dialling(pn, from) {
Some(v) => v
None => "(unknown region)"
}
show("from " + from, written)
}
}
}
head("region facts come from the table")
let facts : Array[String] = ["US", "JP", "DE", "IT"]
for code in facts {
fact(code)
}
}