de.schegge.phone.PhoneNumberFormatterBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of telephone Show documentation
Show all versions of telephone Show documentation
A Java API for national and international phone numbers
The newest version!
package de.schegge.phone;
import de.schegge.phone.PhoneNumberFormatterContext.Parts;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class PhoneNumberFormatterBuilder {
private final PhoneNumberFormatterBuilder parent;
private PhoneNumberFormatterBuilder active = this;
List parts = new ArrayList<>();
PhoneNumberFormatterBuilder() {
parent = null;
// + = internationalDialingPrefix
// iii = internationalDialingPrefix (LOCALE: by country validated)
// III = iii|+
// ccc = countryCallingCode (LOCALE: by country validated)
// aaa = nationalAccessCode (LOCALE: by country validated)
// nnn = nationalDestinationCode
// sss = subscriberNumber
// eee = extension
// bbb = explicit block with range [min-max] with max size b=10, bb=100, bbb=1000 (min >= 0, min < max, max <= max size)
// BBB = same as bbb but with brackets
// XXX = implicit block with range [0-10^size]
// xxx = same as XXX but with brackets
// ZZZ = XXX|BBB, formatted as BBB
// zzz = xxx|bbb, formatted as bbb
// - = as it
// \s = as it
// (|) = as it
}
PhoneNumberFormatterBuilder(PhoneNumberFormatterBuilder parent) {
this.parent = parent;
}
PhoneNumberFormatter toFormatter(String format) {
return toFormatter(format, Locale.getDefault());
}
PhoneNumberFormatter toFormatter(String format, Locale locale) {
Localization localization = Localization.getLocalizations(locale);
return toFormatter(format, locale, localization.internationalDialingPrefix(), localization.nationalAccessCode());
}
PhoneNumberFormatter toFormatter(String format, Locale locale, String internationalDialingPrefix, String nationalAccessCode) {
int pos = 0;
while (pos < format.length()) {
char c = format.charAt(pos);
if ('a' <= c && 'z' >= c || 'I' == c || 'B' == c || 'X' == c || 'Z' == c) {
int length = 1;
pos++;
while (pos < format.length() && format.charAt(pos) == c) {
pos++;
length++;
}
addMultiCharacterPart(c, length);
--pos;
} else if (c == '+') {
active.parts.add(new PlusPart());
} else if (' ' == c || '(' == c || ')' == c || '-' == c) {
active.parts.add(new LiteralPart(c));
} else if ('[' == c) {
startOptional();
} else if (']' == c) {
endOptional();
} else {
throw new IllegalArgumentException("parse exception: " + pos + " " + c);
}
pos++;
}
if (active.parent != null) {
throw new IllegalArgumentException("optional start without end");
}
return new PhoneNumberFormatter(parts, locale, internationalDialingPrefix, nationalAccessCode);
}
private void addMultiCharacterPart(char c, int length) {
switch (c) {
case 'i':
active.parts.add(new LocalizedPart(Parts.INTERNATIONAL_DIALING_PREFIX.ordinal()));
break;
case 'I':
active.parts.add(new InternationalDialingPrefixOrPlus());
break;
case 'c':
active.parts.add(new NumberPart(Parts.COUNTRY_CALLING_CODE.ordinal()));
break;
case 'a':
active.parts.add(new LocalizedPart(Parts.NATIONAL_ACCESS_CODE.ordinal()));
break;
case 'n':
active.parts.add(new NumberPart(Parts.NATIONAL_DESTINATION_CODE.ordinal()));
break;
case 's':
active.parts.add(new NumberPart(Parts.SUBSCRIBER_NUMBER.ordinal()));
break;
case 'e':
active.parts.add(new NumberPart(Parts.EXTENSION.ordinal()));
break;
case 'b':
active.parts.add(new BlockPart(Parts.BLOCK_START.ordinal(), length));
active.parts.add(new LiteralPart('-'));
active.parts.add(new BlockPart(Parts.BLOCK_END.ordinal(), length));
break;
case 'B':
active.parts.add(new LiteralPart('['));
active.parts.add(new BlockPart(Parts.BLOCK_START.ordinal(), length));
active.parts.add(new LiteralPart('-'));
active.parts.add(new BlockPart(Parts.BLOCK_END.ordinal(), length));
active.parts.add(new LiteralPart(']'));
break;
case 'x':
active.parts.add(new BlockPatternPart(length));
break;
case 'X':
active.parts.add(new LiteralPart('['));
active.parts.add(new BlockPatternPart(length));
active.parts.add(new LiteralPart(']'));
break;
case 'z':
active.parts.add(new BlockPatternOrBlockPart(length));
break;
case 'Z':
active.parts.add(new LiteralPart('['));
active.parts.add(new BlockPatternOrBlockPart(length));
active.parts.add(new LiteralPart(']'));
break;
default:
throw new IllegalArgumentException("Unknown pattern letter: " + c);
}
}
private void startOptional() {
active = new PhoneNumberFormatterBuilder(active);
}
private void endOptional() {
if (active.parent == null) {
throw new IllegalArgumentException("optional end without start");
}
if (!active.parts.isEmpty()) {
active.parent.parts.add(new CompositePart(active.parts));
}
active = active.parent;
}
interface PhoneNumberPart {
int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos);
boolean format(PhoneNumberFormatterContext context, StringBuilder builder);
boolean isMissing(PhoneNumberFormatterContext context);
}
static class BlockPart implements PhoneNumberPart {
private final int part;
private final int length;
BlockPart(int part, int length) {
this.part = part;
this.length = length;
}
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
if (sequence.length() == pos) {
return ~pos;
}
int start = pos++;
while (pos < sequence.length() && Character.isDigit(sequence.charAt(pos))) {
++pos;
}
if (pos - start > length) {
return ~pos;
}
context.parts[part] = sequence.subSequence(start, pos);
return pos;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
builder.append(context.parts[part]);
return true;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return context.parts[part] == null;
}
}
static class BlockPatternPart implements PhoneNumberPart {
private final int length;
public BlockPatternPart(int length) {
this.length = length;
}
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
if (sequence.length() == pos) {
return ~pos;
}
int start = pos;
while (pos < sequence.length() && Character.toLowerCase(sequence.charAt(pos)) == 'x') {
++pos;
}
if (pos - start > length) {
return ~pos;
}
context.parts[Parts.BLOCK_PATTERN.ordinal()] = sequence.subSequence(start, pos);
return pos;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
CharSequence blockStart = context.parts[Parts.BLOCK_START.ordinal()];
CharSequence blockEnd = context.parts[Parts.BLOCK_END.ordinal()];
if (blockStart.chars().allMatch(c -> c == '0') && blockEnd.chars().allMatch(c -> c == '9')) {
builder.append("X".repeat(blockStart.length()));
} else {
builder.append(blockStart).append('-').append(blockEnd);
}
return true;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return context.parts[Parts.BLOCK_START.ordinal()] == null || context.parts[Parts.BLOCK_END.ordinal()] == null;
}
}
private static class BlockPatternOrBlockPart implements PhoneNumberPart {
private final BlockPatternPart blockPattern;
private final CompositePart block;
private final int length;
public BlockPatternOrBlockPart(int length) {
this.length = length;
blockPattern = new BlockPatternPart(length);
block = new CompositePart(List.of(new BlockPart(Parts.BLOCK_START.ordinal(), length), new LiteralPart('-'), new BlockPart(Parts.BLOCK_END.ordinal(), length)));
}
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
int newPos = blockPattern.parse(context, sequence, pos);
if (newPos > pos) {
return newPos;
}
newPos = block.parse(context, sequence, pos);
if (newPos == pos) {
return ~newPos;
}
if (length * 2 + 1 + pos < newPos) {
return ~newPos;
}
return newPos;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
CharSequence blockStart = context.parts[Parts.BLOCK_START.ordinal()];
CharSequence blockEnd = context.parts[Parts.BLOCK_END.ordinal()];
builder.append(blockStart).append('-').append(blockEnd);
return true;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return blockPattern.isMissing(context) || block.isMissing(context);
}
}
static class NumberPart implements PhoneNumberPart {
private final int part;
NumberPart(int part) {
this.part = part;
}
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
if (sequence.length() == pos) {
return ~pos;
}
int start = pos++;
while (pos < sequence.length() && Character.isDigit(sequence.charAt(pos))) {
++pos;
}
context.parts[part] = sequence.subSequence(start, pos);
return pos;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
builder.append(context.parts[part]);
return true;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return context.parts[part] == null;
}
}
static class PlusPart implements PhoneNumberPart {
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int offset) {
if (sequence.length() == offset) {
return ~offset;
}
if (sequence.charAt(offset) == '+') {
context.parts[0] = "+";
return ++offset;
}
return ~offset;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
builder.append('+');
return true;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return context.parts[Parts.INTERNATIONAL_DIALING_PREFIX.ordinal()] == null;
}
}
private record LiteralPart(char literal) implements PhoneNumberPart {
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
if (sequence.length() == pos) {
return ~pos;
}
if (sequence.charAt(pos) == literal) {
return ++pos;
}
return ~pos;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
builder.append(literal);
return false;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return false;
}
}
private record LocalizedPart(int part) implements PhoneNumberPart {
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
if (sequence.length() == pos) {
return ~pos;
}
String value = getLocalizedValue(context);
if (value.contentEquals(sequence.subSequence(pos, pos + value.length()))) {
context.parts[part] = value;
return pos + value.length();
}
return ~pos;
}
private String getLocalizedValue(PhoneNumberFormatterContext context) {
return switch (part) {
case 0 -> Localization.getLocalizations(context.formatter.getLocale()).internationalDialingPrefix();
case 2 -> Localization.getLocalizations(context.formatter.getLocale()).nationalAccessCode();
default -> throw new IllegalArgumentException();
};
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
builder.append(getLocalizedValue(context));
return false;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return context.parts[part] == null;
}
}
private static class InternationalDialingPrefixOrPlus implements PhoneNumberPart {
private final PlusPart plusPart = new PlusPart();
private final LocalizedPart internationalDialingPrefix = new LocalizedPart(0);
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
int newPos = plusPart.parse(context, sequence, pos);
if (newPos > 0) {
return newPos;
}
return internationalDialingPrefix.parse(context, sequence, pos);
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
return plusPart.format(context, builder);
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return plusPart.isMissing(context);
}
}
private record CompositePart(List parts) implements PhoneNumberPart {
@Override
public int parse(PhoneNumberFormatterContext context, CharSequence sequence, int pos) {
int position = pos;
for (PhoneNumberPart part : parts) {
pos = part.parse(context, sequence, pos);
if (pos < 0) {
return position;
}
}
return pos;
}
@Override
public boolean format(PhoneNumberFormatterContext context, StringBuilder builder) {
if (isMissing(context)) {
return true;
}
for (PhoneNumberPart part : parts) {
part.format(context, builder);
}
return true;
}
@Override
public boolean isMissing(PhoneNumberFormatterContext context) {
return parts.stream().anyMatch(part -> part.isMissing(context));
}
}
}