I wrote the following code
import java.io.IOException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Produces a string suitable for representing phone numbers by formatting them according with the format string
*/
@Slf4j
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(builderMethodName = "builderInternal")
public class PhoneNumberFormatter {
/** Format string used to represent phone numbers */
@Getter @Builder.Default private String formatString = "(###)###-";
/** String used to represent null phone numbers */
@Getter @Builder.Default private String nullString = "---";
/** Pattern used for substitutions of phone numbers.
* Pounds (#) are replaced by phone digits unless they are escaped (\#) */
private static final Pattern PATTERN = Pattern.compile("\\\\#|#");
/** Returns a new formatter with default parameters */
public static PhoneNumberFormatter of() {
return builder().build();
}
/** Returns a new formatter with the format string */
public static PhoneNumberFormatter of(String formatString) {
return builder(formatString).build();
}
/** Create a builder object. Notice that lombok does most of the work in builderInternal
* but we want to pass the format string to the builder method directly */
public static PhoneNumberFormatterBuilder builder(String formatString) {
Objects.requireNonNull(formatString, "formatString");
return builder().formatString(formatString);
}
/** Create a new builder with default parameters. Notice that builderInternal is created by lombok */
public static PhoneNumberFormatterBuilder builder() {
return builderInternal();
}
/** Return a formatted string corresponding to the given user */
public String format(String phoneNumber) {
StringBuilder buf = new StringBuilder(32);
formatTo(phoneNumber, buf);
return buf.toString();
}
/** Append a formatted string corresponding to the given user to the given appendable */
public void formatTo(String phoneNumber, Appendable appendable) {
Objects.requireNonNull(appendable, "appendable");
if (appendable instanceof StringBuilder) {
doFormat(phoneNumber, (StringBuilder) appendable);
} else {
// buffer output to avoid writing to appendable in case of error
StringBuilder buf = new StringBuilder(32);
doFormat(phoneNumber, buf);
try {
appendable.append(buf);
} catch (IOException e) {
log.warn("Could not append phone number {} to {appendable}", phoneNumber, e);
}
}
}
/** Performs regex search and replacement of digits */
private void doFormat(String phoneNumber, StringBuilder result) {
if (phoneNumber == null) {
result.append(nullString);
return;
}
Matcher matcher = PATTERN.matcher(formatString);
int digitIndex = 0;
while (matcher.find()) {
if (matcher.group().equals("\\#")) {
matcher.appendReplacement(result, "#");
} else if (digitIndex < phoneNumber.length()) {
matcher.appendReplacement(result, String.valueOf(phoneNumber.charAt(digitIndex)));
digitIndex++;
} else {
break; // Stop if there are no more digits in the phone number
}
}
matcher.appendTail(result);
result.append(phoneNumber.substring(digitIndex));
}
}
This is some test code that exemplifies the usage:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
public class PhoneNumberFormatterTest {
@Test
public void nullArgsTest() {
assertThrows(NullPointerException.class, () -> PhoneNumberFormatter.of(null),
"Expected using null as a format string to fail, but it didn't");
PhoneNumberFormatter f = PhoneNumberFormatter.of();
assertEquals(f.getNullString(), f.format(null));
f = PhoneNumberFormatter.builder().nullString("null phone").build();
assertEquals("null phone", f.format(null));
assertThrows(NullPointerException.class, () -> PhoneNumberFormatter.of().formatTo("12345", null),
"Expected appending to a null appender to fail, but it didn't");
}
@Test
public void validTests() {
PhoneNumberFormatter f = PhoneNumberFormatter.of();
String p = "1234567890";
assertEquals("(123)456-7890", f.format(p));
StringBuffer s = new StringBuffer("Hello this is a phone number: ");
f.formatTo(p, s);
assertEquals("Hello this is a phone number: (123)456-7890", s.toString());
f = PhoneNumberFormatter.of("(###)###-####");
assertEquals("(123)456-7890", f.format(p));
assertEquals("(555)456-7890", f.format("5554567890"));
f = PhoneNumberFormatter.of("(###) ###-####");
assertEquals("(123) 456-7890", f.format(p));
f = PhoneNumberFormatter.of("+1(###)###-####");
assertEquals("+1(123)456-7890", f.format(p));
f = PhoneNumberFormatter.of("\\#(###)\\####-");
assertEquals("#(123)#456-7890", f.format(p));
f = PhoneNumberFormatter.of("\\\\#(###)\\####-");
assertEquals("\\#(123)#456-7890", f.format(p));
f = PhoneNumberFormatter.of("(###)###-####-####");
assertEquals("(123)456-7890-####", f.format(p));
}
}
longis not a good idea. Phone numbers are really a kind of labels, not numbers that you'd want to do calculations with. What if a phone number starts with 0 - you couldn't store that in a numeric type.