EIN.java

package com.maybeitssquid.tin.us;

import com.maybeitssquid.tin.InvalidTINException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * An Employer Identification Number (EIN), also known as a Federal Tax Identification Number.
 *
 * <p>EINs are nine-digit numbers in the format {@code CC-SSSSSSS} where:
 *
 * <ul>
 *   <li>CC is the campus code (2 digits) indicating the IRS campus that issued the number
 *   <li>SSSSSSS is the serial number (7 digits)
 * </ul>
 *
 * <p>This class provides automatic masking when formatted. By default, the leading digits of the
 * EIN are masked. Use the alternate form ({@code %#s}) to show delimiters:
 *
 * <pre>{@code
 * EIN ein = new EIN("12-3456789");
 * String.format("%s", ein);      // Returns "#####6789"
 * String.format("%#s", ein);     // Returns "##-###6789"
 * String.format("%.4s", ein);    // Returns "#####6789"
 * String.format("%#.4s", ein);   // Returns "##-###6789"
 * }</pre>
 *
 * <p>This class is final to prevent subclasses from undermining the security protections.
 *
 * @see SSN
 * @see UsTIN
 */
public final class EIN extends UsTIN {
  private static final String CAMPUS = digits("campus", 2);
  private static final String SERIAL = digits("serial", 7);

  private static final Pattern CAMPUS_PATTERN = Pattern.compile("^%s$".formatted(CAMPUS));
  private static final Pattern SERIAL_PATTERN = Pattern.compile("^%s$".formatted(SERIAL));
  private static final Pattern EIN_PATTERN =
      Pattern.compile("^%1$s%3$s?%2$s$".formatted(CAMPUS, SERIAL, DELIMITER));

  /**
   * Creates an EIN from individual string segments.
   *
   * @param campus the 2-digit campus code
   * @param serial the 7-digit serial number
   * @throws InvalidTINException if any segment is invalid
   */
  public EIN(String campus, String serial) {
    super(validate(campus, serial));
  }

  /**
   * Creates an EIN from individual integer segments.
   *
   * @param campus the campus code (1-99)
   * @param serial the serial number (1-9999999)
   * @throws InvalidTINException if any segment is out of range
   */
  public EIN(int campus, int serial) {
    this("%02d".formatted(campus), "%07d".formatted(serial));
  }

  /**
   * Parses an EIN from a formatted string.
   *
   * <p>Accepts formats: {@code ##-#######} or {@code #########}
   *
   * @param value the EIN string to parse
   * @throws InvalidTINException if the format is invalid or value is null
   */
  public EIN(final CharSequence value) {
    super(parse(value));
  }

  private static CharSequence[] validate(final CharSequence campus, final CharSequence serial) {
    if (!CAMPUS_PATTERN.matcher(campus).matches())
      throw new InvalidTINException("Invalid EIN, group part must be 2 digits");
    if (!SERIAL_PATTERN.matcher(serial).matches())
      throw new InvalidTINException("Invalid EIN, serial part must be 7 digits");
    return new CharSequence[] {campus, serial};
  }

  private static CharSequence[] parse(final CharSequence raw) {
    final Matcher matcher = EIN_PATTERN.matcher(raw);
    if (!matcher.matches()) throw new InvalidTINException("Invalid EIN");
    return new CharSequence[] {matcher.group("campus"), matcher.group("serial")};
  }
}