// Rendering a parsed number back to text.
//
// Four renderings, all built from the parsed value rather than the original
// input so they are stable no matter how the number was written:
//
// * E.164 +8613123456789
// * international +86 131 2345 6789
// * national (020) 7946 0958 / 090-1234-5678
// * RFC 3966 tel:+8613123456789;ext=42
//
// The two grouped renderings use the plans' own formatting rules
// (`plan_data.mbt` carries them, `data/numbering-plans.json` is where they come
// from). A rule is a sequence of capture groups over digit runs plus the text
// between them, for example `(\d{3})(\d{4})` with `$1-$2`; a number is rendered
// by the first rule whose published leading digits match and whose shape fits.
// When no rule applies the number falls back to its ungrouped form, so a plan
// without formatting data still renders.
///|
/// One capture group of a formatting rule: a digit run of at least `min` and at
/// most `max` digits, where a negative `max` means unbounded.
priv struct FormatGroup {
min : Int
max : Int
}
///|
/// Parse a formatting rule's pattern. The data pipeline has already restricted
/// these to a sequence of `(\d...)` groups, so a pattern outside that shape is a
/// hand-written rule and simply yields no grouping.
fn parse_format_groups(pattern : String) -> Array[FormatGroup]? {
let cs = chars_of(pattern)
let out : Array[FormatGroup] = []
let mut i = 0
while i < cs.length() {
if cs[i] != '(' {
return None
}
i = i + 1
if i + 1 >= cs.length() || cs[i] != '\\' || cs[i + 1] != 'd' {
return None
}
i = i + 2
let mut min = 1
let mut max = 1
if i < cs.length() && cs[i] == '{' {
let (lo, p) = match parse_int(cs, i + 1) {
None => return None
Some(v) => v
}
min = lo
max = lo
i = p
if i < cs.length() && cs[i] == ',' {
i = i + 1
if i < cs.length() && cs[i] == '}' {
max = -1
} else {
let (hi, p2) = match parse_int(cs, i) {
None => return None
Some(v) => v
}
max = hi
i = p2
}
}
if i >= cs.length() || cs[i] != '}' {
return None
}
i = i + 1
}
if i >= cs.length() || cs[i] != ')' {
return None
}
i = i + 1
out.push({ min, max })
}
if out.length() == 0 {
None
} else {
Some(out)
}
}
///|
/// Split `total` digits over the groups, left to right and greedily, the way a
/// regex engine's greedy quantifiers would. None when the shapes cannot cover
/// the number.
fn distribute_digits(groups : Array[FormatGroup], total : Int) -> Array[Int]? {
let mut need = 0
for g in groups {
need = need + g.min
}
if total < need {
return None
}
let lens : Array[Int] = []
let mut remaining = total
let mut i = 0
while i < groups.length() {
let g = groups[i]
let mut rest_min = 0
let mut j = i + 1
while j < groups.length() {
rest_min = rest_min + groups[j].min
j = j + 1
}
let avail = remaining - rest_min
let take = if g.max < 0 {
avail
} else if avail > g.max {
g.max
} else {
avail
}
if take < g.min {
return None
}
lens.push(take)
remaining = remaining - take
i = i + 1
}
if remaining != 0 {
return None
}
Some(lens)
}
///|
/// Replace `$1`..`$9` in `rule` with the corresponding digit groups.
fn apply_format_rule(rule : String, digits : Array[Char], lens : Array[Int]) -> String {
let starts : Array[Int] = []
let mut acc = 0
for n in lens {
starts.push(acc)
acc = acc + n
}
let buf = StringBuilder()
let cs = chars_of(rule)
let mut i = 0
while i < cs.length() {
let c = cs[i]
if c == '$' && i + 1 < cs.length() && cs[i + 1] >= '1' && cs[i + 1] <= '9' {
let idx = cs[i + 1].to_int() - '1'.to_int()
if idx < lens.length() && starts[idx] + lens[idx] <= digits.length() {
let mut k = starts[idx]
let end = starts[idx] + lens[idx]
while k < end {
buf.write_char(digits[k])
k = k + 1
}
}
i = i + 2
} else {
buf.write_char(c)
i = i + 1
}
}
buf.to_string()
}
///|
/// Expand a national-prefix rule: `$NP` becomes the plan's trunk prefix and
/// `$FG` the formatted number. Only the first group is part of the rule: the
/// published rules wrap the prefix and the leading group (`($NP$FG)` for
/// Australia, `($FG)` for Brazil), and the rest of the format follows outside
/// the wrapper.
fn expand_prefix_rule(rule : String, prefix : String) -> String {
let cs = chars_of(rule)
let buf = StringBuilder()
let mut i = 0
while i < cs.length() {
if cs[i] == '$' && i + 2 < cs.length() {
let a = cs[i + 1]
let b = cs[i + 2]
if a == 'N' && b == 'P' {
buf.write_string(prefix)
i = i + 3
continue
}
if a == 'F' && b == 'G' {
buf.write_string("$1")
i = i + 3
continue
}
}
buf.write_char(cs[i])
i = i + 1
}
buf.to_string()
}
///|
/// The part of a format string after its first group: `$1 $2-$3` becomes
/// ` $2-$3`. This is what gets appended to a prefix rule, whose `$FG` only
/// stands for the leading group.
fn format_tail(format : String) -> String {
let cs = chars_of(format)
let buf = StringBuilder()
let mut i = 0
let mut removed = false
while i < cs.length() {
if !removed && cs[i] == '$' && i + 1 < cs.length() && cs[i + 1] == '1' {
removed = true
i = i + 2
continue
}
buf.write_char(cs[i])
i = i + 1
}
buf.to_string()
}
///|
/// True when the formatting rule applies to the number: its published
/// leading-digit pattern has to match where the number starts. The pattern is
/// compiled once and kept in the rule, and the match is a single pass over the
/// input rather than one match per prefix.
fn format_leading_matches(f : PhoneFormat, national_number : String) -> Bool {
if f.leading.length() == 0 {
return true
}
// The metadata lists the same rule once per progressively longer leading
// pattern; the last one is the most specific and decides.
let pattern = f.leading[f.leading.length() - 1]
let compiled = match f.leading_rule.val {
Some(c) => c
None => {
let c = compile_pattern(pattern)
f.leading_rule.val = Some(c)
c
}
}
if compiled.start < 0 {
return true
}
run_pattern_prefix(compiled, national_number)
}
///|
/// Render a national number through a plan's formatting rules. None when no rule
/// fits, which the callers turn into the ungrouped form.
fn render_with_formats(
r : Region,
national_number : String,
international : Bool,
) -> String? {
let digits = chars_of(national_number)
for c in digits {
if !is_digit(c) {
return None
}
}
for f in formats_of(r) {
if !format_leading_matches(f, national_number) {
continue
}
let groups = match parse_format_groups(f.pattern) {
None => continue
Some(g) => g
}
let lens = match distribute_digits(groups, digits.length()) {
None => continue
Some(l) => l
}
let rule = if international {
if f.intl_format == "NA" {
continue
}
if f.intl_format != "" {
f.intl_format
} else {
f.format
}
} else if f.prefix_rule != "" {
expand_prefix_rule(f.prefix_rule, r.national_prefix) + format_tail(f.format)
} else {
f.format
}
return Some(apply_format_rule(rule, digits, lens))
}
None
}
///|
/// 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.
pub fn format_e164(pn : PhoneNumber) -> String {
"+" + pn.country_code.to_string() + pn.national_number
}
///|
/// The international rendering, grouped by the plan that owns the number:
/// `+44 20 7946 0958`. Falls back to the ungrouped form when the plan publishes
/// no rule for the number's shape.
pub fn format_international(pn : PhoneNumber) -> String {
let base = "+" + pn.country_code.to_string() + " "
match region_of_number(pn) {
None => base + pn.national_number
Some(code) =>
match find_region(code) {
None => base + pn.national_number
Some(r) =>
match render_with_formats(r, pn.national_number, true) {
None => base + pn.national_number
Some(v) => base + v
}
}
}
}
///|
/// The number as dialled inside `region`: the plan's trunk prefix and the
/// national number, grouped by the plan's own formatting rules —
/// `(201) 555-0123`, `020 7946 0958`, `090-1234-5678`. None when the region is
/// unknown or uses a different calling code than the number. A plan that
/// publishes no rule for the shape falls back to trunk prefix plus ungrouped
/// number.
pub fn format_national(pn : PhoneNumber, region : String) -> String? {
match find_region(region) {
None => None
Some(r) =>
if r.calling_code != pn.country_code {
None
} else {
match render_with_formats(r, pn.national_number, false) {
Some(v) => Some(v)
None => Some(r.national_prefix + pn.national_number)
}
}
}
}
///|
/// True when the string is a run of digits, which is what a dial-out prefix has
/// to be to be usable.
fn is_all_digits(s : String) -> Bool {
if s == "" {
return false
}
for c in s {
if !is_digit(c) {
return false
}
}
true
}
///|
/// The number as it is dialled from `from_region`: that region's international
/// dial-out prefix followed by the calling code and the national number —
/// `011 442079460958` from the NANP, `00 442079460958` from most of Europe.
///
/// When the number is inside `from_region`, what is dialled there is the
/// national form, so that is what comes back. None when the region is unknown; a
/// plan whose dial-out prefix is published as an expression rather than as
/// digits needs a choice this library cannot make for the caller, so the `+`
/// form comes back instead.
pub fn format_dialling(pn : PhoneNumber, from_region : String) -> String? {
match find_region(from_region) {
None => None
Some(from) =>
if from.calling_code == pn.country_code {
format_national(pn, from_region)
} else if is_all_digits(from.idd) {
Some(from.idd + " " + pn.country_code.to_string() + pn.national_number)
} else {
Some(format_e164(pn))
}
}
}
///|
/// The RFC 3966 `tel:` URI. The extension, when present, becomes `;ext=`. The
/// subscriber is the ungrouped E.164 number, as the grammar expects.
pub fn format_rfc3966(pn : PhoneNumber) -> String {
let base = "tel:" + format_e164(pn)
if pn.extension == "" {
base
} else {
base + ";ext=" + pn.extension
}
}