Sign in

    dialnumber

    Phone number parsing, validation and formatting for MoonBit: E.164 and RFC 3966, international and national dialing, calling-code and region lookup across 51 numbering plans

    phone
    e164
    telephone
    msisdn
    dialing
    rfc3966
    itu-t
    formatting
    validation
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    11 hours ago
    Downloads
    3

    #dialnumber

    Phone number parsing, validation and formatting for MoonBit, built on the national numbering plans behind ITU-T E.164.

    A phone number written by a person is a mess of spaces, dashes, parentheses, trunk prefixes and country codes, and the same number can be written a dozen ways. dialnumber turns any of them into one value, checks it against the plan, and renders it back in whichever of the standard shapes you need.

    #What it does

    • Parse an E.164 number (+81 90-1234-5678), a number dialled internationally (011 44 20 7946 0958 from the NANP), or a number dialled nationally with a region hint (090-1234-5678 in Japan) into a PhoneNumber.
    • Check the structure against the plan: the calling code is one the table knows, and the national number's length is one the plan uses.
    • List the number types a length admits — fixed-line, mobile, toll-free, premium-rate, shared-cost, voip, uan, personal, pager, voicemail.
    • Format to E.164, international, national, or an RFC 3966 tel: URI.
    • Look up regions by code or by calling code, with the plan's international and national prefixes and its national-number lengths.
    • Read and write RFC 3966 tel: URIs, including ;ext= and the ;phone-context= local-number form.

    The table covers 51 numbering plans and every public function is total: bad input comes back as None or false, never as an exception or a panic.

    #What it deliberately does not do

    This is the boundary that matters, so it is stated up front rather than buried.

    dialnumber validates structure, not assignment. It can tell you that +81 90-1234-5678 is a well-formed Japanese mobile but not that the line exists, is reachable, or belongs to anyone. Deciding that needs each plan's leading-digit ranges and its allocation records, which this library does not carry.

    So there is no is_valid that claims more than the data supports. The check is named is_possible, the same word libphonenumber uses for the length test, and possible_types returns a candidate set rather than a verdict, because several types can share one length.

    For the same reason there is no locale-aware digit grouping. Rendering 201 555 0123 correctly needs the leading-digit patterns that are out of scope; a generic grouper would be wrong exactly where it looked most confident, which is worse than leaving the caller to group for display.

    #Relationship to other MoonBit libraries

    The ecosystem has plenty of validators, and none of them do this. moon_zod, moonschema and jsonschema validate JSON against a schema, including email and uuid string formats; they do not know about telephone numbering. moovalid and maru are validator-combinator frameworks. mooncontract validates OpenAPI contracts, and moonmrz reads the ICAO 9303 machine-readable zone of a passport. dialnumber sits alongside them: it takes a phone number in and gives you its parts, its plan, and its canonical renderings, with no I/O and no dependencies.

    #Install

    moon add sssssurf/dialnumber

    #Quick start

    ///|
    let pn = @dialnumber.parse("011 44 20 7946 0958", "US")

    ///|
    match pn {
    Some(n) => {
    @dialnumber.format_e164(n) // "+442079460958"
    @dialnumber.format_national(n, "GB") // Some("02079460958")
    @dialnumber.is_possible(n) // true
    @dialnumber.region_of_number(n) // Some("GB")
    }
    None => ()
    }

    #Command line

    The cmd/main package is a small front end. Every subcommand prints a plain report, and the text below is recorded from a real run.

    moon run cmd/main -- parse "+81 90-1234-5678" moon run cmd/main -- possible "090-1234-5678" JP moon run cmd/main -- format "090-1234-5678" JP moon run cmd/main -- types "+86 131 2345 6789" moon run cmd/main -- region "+442079460958" moon run cmd/main -- info JP moon run cmd/main -- cc de moon run cmd/main -- tel "tel:+1-201-555-0123;ext=42" moon run cmd/main -- list

    A few of those, with the output they produce:

    $ moon run cmd/main -- parse "+81 90-1234-5678" country_code: 81 national_number: 9012345678 e164: +819012345678 region: JP

    $ moon run cmd/main -- info JP region: JP name: Japan calling_code: 81 idd: 010 national_prefix: 0 lengths: 8,9,10,11,12,13,14,15,16,17

    $ moon run cmd/main -- types "+86 131 2345 6789" types: fixed-line,mobile,shared-cost

    #API

    FunctionPurpose
    parse(input, region)read any written form into a PhoneNumber?
    parse_e164(input)read only a +-prefixed number
    parse_national(input, region)read a number as dialled in a region
    is_possible(pn)calling code known and length used by the plan
    is_possible_in_region(pn, region)the same, against one named region
    is_possible_number(input, region)parse then check, in one call
    possible_types(pn)number types the length admits
    possible_types_in_region(pn, region)the same, within one region
    region_of_number(pn)the region a number resolves to
    format_e164(pn)+8613123456789
    format_international(pn)+86 13123456789
    format_national(pn, region)trunk prefix plus national number
    format_rfc3966(pn)tel:+8613123456789;ext=42
    parse_tel_uri(uri)read a tel: URI
    to_tel_uri(pn)write a tel: URI
    region_codes()every region code in the table
    region_exists(code)is the code in the table
    canonical_region_code(code)the table's spelling of a code
    region_name(code)English name
    calling_code(code)E.164 calling code
    idd_prefix(code)international dial-out prefix
    national_prefix(code)trunk prefix, or None
    possible_lengths(code)national-number lengths
    region_by_calling_code(cc)one region for a calling code
    regions_by_calling_code(cc)every region for a calling code

    The NumberType enum names the ten categories. The PhoneNumber struct holds country_code, national_number (a string, so a significant leading zero survives), and extension.

    #Where the data comes from

    The numbering-plan table is derived from Google's libphonenumber metadata (PhoneNumberMetadata.xml, Apache-2.0), which tracks the plans themselves. Only the length rules are reproduced here; the leading-digit patterns are the part left out, which is what keeps is_possible a length test.

    #Tests

    50 tests cover the public API, the package-private helpers, and robustness against malformed input. The vectors are real numbers from the plans' published examples, and the robustness suite feeds the parser junk, 500-digit numbers and lone separators to show that nothing panics.

    moon test

    #License

    Apache-2.0. See LICENSE.

    NumberType

    pub(all) enum NumberType {
    FixedLine
    Mobile
    TollFree
    PremiumRate
    SharedCost
    Voip
    Uan
    PersonalNumber
    Pager
    Voicemail
    } derive(Eq,
    Debug
    )

    The categories a numbering plan uses to tell one kind of line from another. A number can fit several at once, so valid.mbt hands back the set that its length admits rather than a single value.
    impl Show for NumberType

    NumberType::equal

    fn NumberType::equal(NumberType, NumberType) -> Bool

    Eq promotes both methods; name them so the promotion is stated rather than implied.

    NumberType::not_equal

    fn NumberType::not_equal(x : NumberType, y : NumberType) -> Bool

    Eq promotes both methods; name them so the promotion is stated rather than implied.

    NumberType::output

    fn NumberType::output(self : NumberType, logger : &Logger) -> Unit

    Show promotes both output and to_string; name them so the promotion is stated rather than implied (the compiler flags the alternative, E0079).

    NumberType::to_repr

    Debug promotes to_repr the same way; name it too.

    NumberType::to_string

    fn NumberType::to_string(self : NumberType) -> String

    Show promotes both output and to_string; name them so the promotion is stated rather than implied (the compiler flags the alternative, E0079).

    PhoneNumber

    pub(all) struct PhoneNumber {
    country_code : Int
    national_number : String
    extension : String
    } derive(Eq)

    A parsed number, independent of how it was written.

    country_code is the E.164 calling code without the leading +. national_number is the significant part of the number as the plan defines it: a string, not an integer, because some plans (Italy for one) require a leading zero that an integer would drop. extension is empty when the input carried none.
    impl Show for PhoneNumber

    PhoneNumber::equal

    fn PhoneNumber::equal(PhoneNumber, PhoneNumber) -> Bool

    Eq promotes both methods; name them so the promotion is stated rather than implied (the compiler flags the alternative, E0079).

    PhoneNumber::new

    fn PhoneNumber::new(country_code : Int, national_number : String) -> PhoneNumber

    Build a number with no extension.

    PhoneNumber::not_equal

    fn PhoneNumber::not_equal(x : PhoneNumber, y : PhoneNumber) -> Bool

    Eq promotes both methods; name them so the promotion is stated rather than implied (the compiler flags the alternative, E0079).

    PhoneNumber::output

    fn PhoneNumber::output(self : PhoneNumber, logger : &Logger) -> Unit

    PhoneNumber::to_string

    fn PhoneNumber::to_string(self : PhoneNumber) -> String

    PhoneNumber::with_extension

    fn PhoneNumber::with_extension(self : PhoneNumber, extension : String) -> PhoneNumber

    A copy with extension attached.

    calling_code

    fn calling_code(code : String) -> Int?

    The E.164 calling code of a region, without a leading +.

    canonical_region_code

    fn canonical_region_code(code : String) -> String?

    The table's own spelling of a region code, or None when it is not known. Useful for echoing a user-supplied code in a canonical form.

    format_e164

    fn format_e164(pn : PhoneNumber) -> String

    The canonical E.164 rendering: +, the calling code, then the national number, with no separators. Any extension is dropped — use format_rfc3966 when the extension must survive.

    format_international

    fn format_international(pn : PhoneNumber) -> String

    The E.164 rendering with a space between the calling code and the national number, which is how the number is usually printed.

    format_national

    fn format_national(pn : PhoneNumber, region : String) -> String?

    The number as dialled inside region: its trunk prefix followed by the national number. None when the region is unknown or uses a different calling code than the number.

    format_rfc3966

    fn format_rfc3966(pn : PhoneNumber) -> String

    The RFC 3966 tel: URI. The extension, when present, becomes ;ext=.

    idd_prefix

    fn idd_prefix(code : String) -> String?

    The international dial-out prefix used inside a region, e.g. "011" for the NANP or "00" for most of Europe.

    is_possible

    fn is_possible(pn : PhoneNumber) -> Bool

    True when a known region uses the number's calling code and admits its length. Streams are not consulted: this says the number could exist in the plan, not that it is assigned.

    is_possible_in_region

    fn is_possible_in_region(pn : PhoneNumber, region : String) -> Bool

    The same check against one named region.

    is_possible_number

    fn is_possible_number(input : String, region : String) -> Bool

    Parse then check, in one call. None becomes false.

    national_prefix

    fn national_prefix(code : String) -> String?

    The trunk prefix dialled before a national number, or None when the plan has none (Italy, Spain, Portugal and others).

    parse

    fn parse(input : String, default_region : String) -> PhoneNumber?

    Parse a number written in any of the accepted shapes.

    default_region is the ISO 3166-1 alpha-2 code the number should be read in when it does not carry a + or an international prefix. Pass "" to accept only self-describing numbers. Returns None when the input cannot be read.

    parse_e164

    fn parse_e164(input : String) -> PhoneNumber?

    Parse a self-describing number: one that starts with +. The region hint is empty, so anything else returns None.

    parse_national

    fn parse_national(input : String, region : String) -> PhoneNumber?

    Parse a number that is written the way it is dialled inside region.

    parse_tel_uri

    fn parse_tel_uri(uri : String) -> PhoneNumber?

    Parse a tel: URI into a PhoneNumber.

    Handles the global form, the local-with-+-context form, and the ;ext= parameter. The scheme and parameter names are matched case-insensitively. Returns None for anything that is not a resolvable tel URI.

    possible_lengths

    fn possible_lengths(code : String) -> Array[Int]?

    The national-number lengths the region uses, sorted, without repeats.

    possible_types

    fn possible_types(pn : PhoneNumber) -> Array[NumberType]

    The number types whose length rule admits this number, within the region it resolves to. Empty when the length fits no category or no region matches. Several types can share a length, so this is a candidate set, not a verdict.

    possible_types_in_region

    fn possible_types_in_region(pn : PhoneNumber, region : String) -> Array[NumberType]

    The number types in one named region whose length rule admits this number.

    region_by_calling_code

    fn region_by_calling_code(cc : Int) -> String?

    One region that uses a calling code, or None when no region does. When several share the code (+1 is both the United States and Canada here) the region marked primary wins; otherwise the first in code order. Use regions_by_calling_code for all of them.

    region_codes

    fn region_codes() -> Array[String]

    Every region code in the table, sorted.

    region_exists

    fn region_exists(code : String) -> Bool

    True when the region code is one the table knows.

    region_name

    fn region_name(code : String) -> String?

    The English name of a region, e.g. "Japan".

    region_of_number

    fn region_of_number(pn : PhoneNumber) -> String?

    The region a parsed number belongs to: one that uses its calling code and admits its length. For a shared calling code the primary region wins. None when no region matches.

    regions_by_calling_code

    fn regions_by_calling_code(cc : Int) -> Array[String]

    Every region that shares a calling code, in table order.

    to_tel_uri

    fn to_tel_uri(pn : PhoneNumber) -> String

    The RFC 3966 URI for a number. Same rendering as format_rfc3966, offered here so the two halves of URI handling sit together.