SSN.java
package com.maybeitssquid.tin.us;
import com.maybeitssquid.tin.InvalidTINException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A Social Security Number (SSN) or Individual Taxpayer Identification Number (ITIN).
*
* <p>SSNs are nine-digit numbers in the format {@code AAA-GG-SSSS} where:
*
* <ul>
* <li>AAA is the area number (3 digits)
* <li>GG is the group number (2 digits)
* <li>SSSS is the serial number (4 digits)
* </ul>
*
* <p>This class provides automatic masking when formatted. By default, the leading digits of the
* SSN are masked. Use the alternate form ({@code %#s}) to show delimiters:
*
* <pre>{@code
* SSN ssn = new SSN("123-45-6789");
* String.format("%s", ssn); // Returns "#####6789"
* String.format("%#s", ssn); // Returns "###-##-6789"
* String.format("%.4s", ssn); // Returns "#####6789"
* String.format("%#.4s", ssn); // Returns "###-##-6789"
* }</pre>
*
* <p>This class is final to prevent subclasses from undermining the security protections.
*
* @see EIN
* @see UsTIN
*/
public final class SSN extends UsTIN {
private static final String AREA = digits("area", 3);
private static final String GROUP = digits("group", 2);
private static final String SERIAL = digits("serial", 4);
private static final Pattern AREA_PATTERN = Pattern.compile("^%s$".formatted(AREA));
private static final Pattern GROUP_PATTERN = Pattern.compile("^%s$".formatted(GROUP));
private static final Pattern SERIAL_PATTERN = Pattern.compile("^%s$".formatted(SERIAL));
private static final Pattern SSN_PATTERN =
Pattern.compile("^%1$s%4$s?%2$s%4$s?%3$s$".formatted(AREA, GROUP, SERIAL, DELIMITER));
/**
* Creates an SSN from individual string segments.
*
* @param area the 3-digit area number
* @param group the 2-digit group number
* @param serial the 4-digit serial number
* @throws InvalidTINException if any segment is invalid
*/
public SSN(final CharSequence area, final CharSequence group, final CharSequence serial) {
super(validate(area, group, serial));
}
/**
* Creates an SSN from individual integer segments.
*
* @param area the area number (1-999)
* @param group the group number (1-99)
* @param serial the serial number (1-9999)
* @throws InvalidTINException if any segment is out of range
*/
public SSN(final int area, final int group, final int serial) {
this("%03d".formatted(area), "%02d".formatted(group), "%04d".formatted(serial));
}
/**
* Parses an SSN from a formatted string.
*
* <p>Accepts formats: {@code ###-##-####} or {@code #########}
*
* @param value the SSN string to parse
* @throws InvalidTINException if the format is invalid or value is null
*/
public SSN(final CharSequence value) {
super(parse(value));
}
private static CharSequence[] validate(
final CharSequence area, final CharSequence group, final CharSequence serial) {
if (!AREA_PATTERN.matcher(area).matches())
throw new InvalidTINException("Invalid SSN, area part must be 3 digits");
if (!GROUP_PATTERN.matcher(group).matches())
throw new InvalidTINException("Invalid SSN, group part must be 2 digits");
if (!SERIAL_PATTERN.matcher(serial).matches())
throw new InvalidTINException("Invalid SSN, serial part must be 4 digits");
return new CharSequence[] {area, group, serial};
}
private static CharSequence[] parse(final CharSequence raw) {
final Matcher matcher = SSN_PATTERN.matcher(raw);
if (!matcher.matches()) throw new InvalidTINException("Invalid SSN");
return new CharSequence[] {
matcher.group("area"), matcher.group("group"), matcher.group("serial")
};
}
}