All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schegge.phone.PhoneNumberFormatterBuilder Maven / Gradle / Ivy

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));
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy