diff --git a/src/main/java/net/datafaker/idnumbers/AlbanianIdNumber.java b/src/main/java/net/datafaker/idnumbers/AlbanianIdNumber.java index 796b5f31f..3aace9b66 100644 --- a/src/main/java/net/datafaker/idnumbers/AlbanianIdNumber.java +++ b/src/main/java/net/datafaker/idnumbers/AlbanianIdNumber.java @@ -18,7 +18,7 @@ public String getInvalid(BaseProviders faker) { } public String getValid(BaseProviders faker) { - LocalDate birthDate = faker.date().birthdayLocalDate(0, 200); + LocalDate birthDate = faker.timeAndDate().birthday(0, 200); boolean female = faker.bool().bool(); String basePart = yy(birthDate.getYear()) + mm(birthDate.getMonthValue(), female) + dd(birthDate.getDayOfMonth()) + sss(faker); return basePart + checksum(basePart); diff --git a/src/main/java/net/datafaker/idnumbers/BulgarianIdNumber.java b/src/main/java/net/datafaker/idnumbers/BulgarianIdNumber.java new file mode 100644 index 000000000..d96844327 --- /dev/null +++ b/src/main/java/net/datafaker/idnumbers/BulgarianIdNumber.java @@ -0,0 +1,62 @@ +package net.datafaker.idnumbers; + +import net.datafaker.providers.base.BaseProviders; + +import java.time.LocalDate; + +/** + * Specification + */ +public class BulgarianIdNumber implements IdNumbers { + private static final int[] CHECKSUM_WEIGHTS = {2, 4, 8, 5, 10, 9, 7, 3, 6}; + private static final int[] EVEN_DIGITS = {0, 2, 4, 6, 8}; + private static final int[] ODD_DIGITS = {1, 3, 5, 7, 9}; + + public String getValid(BaseProviders faker) { + String basePart = basePart(faker); + return basePart + checksum(basePart); + } + + public String getInvalid(BaseProviders faker) { + String basePart = basePart(faker); + return basePart + (checksum(basePart) + 1) % 10; + } + + private String basePart(BaseProviders faker) { + LocalDate birthDate = faker.timeAndDate().birthday(0, 200); + boolean female = faker.bool().bool(); + return yy(birthDate) + mm(birthDate) + dd(birthDate) + order(faker, female); + } + + private String yy(LocalDate birthDate) { + return "%02d".formatted(birthDate.getYear() % 100); + } + + private String mm(LocalDate birthDate) { + int monthAddition = birthDate.getYear() < 1900 ? 20 : + birthDate.getYear() >= 2000 ? 40 : 0; + return "%02d".formatted(birthDate.getMonthValue() + monthAddition); + } + + private String dd(LocalDate birthDate) { + return "%02d".formatted(birthDate.getDayOfMonth()); + } + + private String order(BaseProviders faker, boolean female) { + int[] availableLastDigits = female ? ODD_DIGITS : EVEN_DIGITS; + int lastDigit = availableLastDigits[faker.number().numberBetween(0, 5)]; + return faker.number().digits(2) + lastDigit; + } + + int checksum(String text) { + int checksum = 0; + for (int i = 0; i < text.length(); i++) { + checksum += digitAt(text, i) * CHECKSUM_WEIGHTS[i]; + } + return (checksum % 11) % 10; + } + + private int digitAt(String text, int index) { + return text.charAt(index) - '0'; + } +} diff --git a/src/main/java/net/datafaker/idnumbers/EstonianIdNumber.java b/src/main/java/net/datafaker/idnumbers/EstonianIdNumber.java index a61c66949..bd4897a96 100644 --- a/src/main/java/net/datafaker/idnumbers/EstonianIdNumber.java +++ b/src/main/java/net/datafaker/idnumbers/EstonianIdNumber.java @@ -29,7 +29,7 @@ public String getValid(final BaseProviders faker) { } private String basePart(BaseProviders faker) { - LocalDate birthday = faker.date().birthdayLocalDate(0, 100); + LocalDate birthday = faker.timeAndDate().birthday(0, 100); return firstDigit(faker) + BIRTHDAY_FORMAT.format(birthday) + faker.number().digits(3); diff --git a/src/main/java/net/datafaker/idnumbers/MoldovaIdNumber.java b/src/main/java/net/datafaker/idnumbers/MoldovaIdNumber.java index b02a72786..4c2fe54f6 100644 --- a/src/main/java/net/datafaker/idnumbers/MoldovaIdNumber.java +++ b/src/main/java/net/datafaker/idnumbers/MoldovaIdNumber.java @@ -27,7 +27,7 @@ public String getInvalid(BaseProviders faker) { } private String basePart(BaseProviders faker) { - var birthday = faker.date().birthdayLocalDate(0, 120); + var birthday = faker.timeAndDate().birthday(0, 120); // IDNP: 2ГГГXXXYYYYYK return firstDigit() + ГГГ(birthday) + XXX(faker) + YYYYY(faker); } diff --git a/src/main/java/net/datafaker/providers/base/IdNumber.java b/src/main/java/net/datafaker/providers/base/IdNumber.java index 7ccdb6077..d048beb7d 100644 --- a/src/main/java/net/datafaker/providers/base/IdNumber.java +++ b/src/main/java/net/datafaker/providers/base/IdNumber.java @@ -1,6 +1,7 @@ package net.datafaker.providers.base; import net.datafaker.idnumbers.AlbanianIdNumber; +import net.datafaker.idnumbers.BulgarianIdNumber; import net.datafaker.idnumbers.EnIdNumber; import net.datafaker.idnumbers.EnZAIdNumber; import net.datafaker.idnumbers.EsMXIdNumber; @@ -16,6 +17,7 @@ import net.datafaker.idnumbers.SvSEIdNumber; import net.datafaker.idnumbers.ZhCnIdNumber; +import java.lang.reflect.InvocationTargetException; import java.time.LocalDate; import java.time.ZoneId; import java.util.Map; @@ -26,7 +28,7 @@ */ public class IdNumber extends AbstractProvider { - private final Map, IdNumbers> map = new ConcurrentHashMap<>(); + private final Map, IdNumbers> providers = new ConcurrentHashMap<>(); protected IdNumber(BaseProviders faker) { super(faker); @@ -41,40 +43,35 @@ public String invalid() { } public String ssnValid() { - EnIdNumber enIdNumber = (EnIdNumber) map.computeIfAbsent(EnIdNumber.class, aClass -> new EnIdNumber()); - return enIdNumber.getValidSsn(faker); + return provider(EnIdNumber.class).getValidSsn(faker); } /** * Specified as #{IDNumber.valid_sv_se_ssn} in sv-SE.yml */ public String validSvSeSsn() { - SvSEIdNumber svSEIdNumber = (SvSEIdNumber) map.computeIfAbsent(SvSEIdNumber.class, aClass -> new SvSEIdNumber()); - return svSEIdNumber.getValidSsn(faker); + return provider(SvSEIdNumber.class).getValidSsn(faker); } /** * Specified as #{IDNumber.valid_en_za_ssn} in en-ZA.yml */ public String validEnZaSsn() { - EnZAIdNumber enZAIdNumber = (EnZAIdNumber) map.computeIfAbsent(EnZAIdNumber.class, aClass -> new EnZAIdNumber()); - return enZAIdNumber.getValidSsn(faker); + return provider(EnZAIdNumber.class).getValidSsn(faker); } /** * Specified as #{IDNumber.invalid_en_za_ssn} in en-ZA.yml */ public String inValidEnZaSsn() { - EnZAIdNumber enZAIdNumber = (EnZAIdNumber) map.computeIfAbsent(EnZAIdNumber.class, aClass -> new EnZAIdNumber()); - return enZAIdNumber.getInValidSsn(faker); + return provider(EnZAIdNumber.class).getInValidSsn(faker); } /** * Specified as #{IDNumber.invalid_sv_se_ssn} in sv-SE.yml */ public String invalidSvSeSsn() { - SvSEIdNumber svSEIdNumber = (SvSEIdNumber) map.computeIfAbsent(SvSEIdNumber.class, aClass -> new SvSEIdNumber()); - return svSEIdNumber.getInvalidSsn(faker); + return provider(SvSEIdNumber.class).getInvalidSsn(faker); } public String singaporeanFin() { @@ -99,18 +96,15 @@ public String singaporeanUinBefore2000() { * @return A Zh-CN id number */ public String validZhCNSsn() { - ZhCnIdNumber zhCnIdNumber = (ZhCnIdNumber) map.computeIfAbsent(ZhCnIdNumber.class, aClass -> new ZhCnIdNumber()); - return zhCnIdNumber.getValidSsn(faker); + return provider(ZhCnIdNumber.class).getValidSsn(faker); } public String validPtNif() { - PtNifIdNumber idNumber = (PtNifIdNumber) map.computeIfAbsent(PtNifIdNumber.class, aClass -> new PtNifIdNumber()); - return idNumber.getValid(faker); + return provider(PtNifIdNumber.class).getValid(faker); } public String invalidPtNif() { - PtNifIdNumber idNumber = (PtNifIdNumber) map.computeIfAbsent(PtNifIdNumber.class, aClass -> new PtNifIdNumber()); - return idNumber.getInvalid(faker); + return provider(PtNifIdNumber.class).getInvalid(faker); } @@ -120,8 +114,7 @@ public String invalidPtNif() { * @return A valid MEX CURP. */ public String validEsMXSsn() { - EsMXIdNumber esMXIdNumber = (EsMXIdNumber) map.computeIfAbsent(EsMXIdNumber.class, aClass -> new EsMXIdNumber()); - return esMXIdNumber.get(faker); + return provider(EsMXIdNumber.class).get(faker); } /** @@ -130,8 +123,7 @@ public String validEsMXSsn() { * @return A valid MEX CURP. */ public String invalidEsMXSsn() { - EsMXIdNumber esMXIdNumber = (EsMXIdNumber) map.computeIfAbsent(EsMXIdNumber.class, aClass -> new EsMXIdNumber()); - return esMXIdNumber.getWrong(faker); + return provider(EsMXIdNumber.class).getWrong(faker); } /** @@ -163,8 +155,7 @@ public String peselNumber(LocalDate birthDate, Gender gender) { * @since 1.8.0 */ public String validKoKrRrn() { - KoKrIdNumber koKrIdNumber = (KoKrIdNumber) map.computeIfAbsent(KoKrIdNumber.class, aClass -> new KoKrIdNumber()); - return koKrIdNumber.getValidRrn(faker); + return provider(KoKrIdNumber.class).getValidRrn(faker); } /** @@ -182,8 +173,7 @@ public String validGeIDNumber() { * @return A valid ID Number */ public String validEstonianPersonalCode() { - EstonianIdNumber idNumber = (EstonianIdNumber) map.computeIfAbsent(EstonianIdNumber.class, aClass -> new EstonianIdNumber()); - return idNumber.getValid(faker); + return provider(EstonianIdNumber.class).getValid(faker); } /** @@ -193,8 +183,7 @@ public String validEstonianPersonalCode() { * @return An invalid ID Number */ public String invalidEstonianPersonalCode() { - EstonianIdNumber idNumber = (EstonianIdNumber) map.computeIfAbsent(EstonianIdNumber.class, aClass -> new EstonianIdNumber()); - return idNumber.getInvalid(faker); + return provider(EstonianIdNumber.class).getInvalid(faker); } /** @@ -203,8 +192,7 @@ public String invalidEstonianPersonalCode() { * @return A valid ID Number */ public String validAlbanianPersonalCode() { - AlbanianIdNumber idNumber = (AlbanianIdNumber) map.computeIfAbsent(AlbanianIdNumber.class, aClass -> new AlbanianIdNumber()); - return idNumber.getValid(faker); + return provider(AlbanianIdNumber.class).getValid(faker); } /** @@ -213,8 +201,7 @@ public String validAlbanianPersonalCode() { * @return An invalid ID Number */ public String invalidAlbanianPersonalCode() { - AlbanianIdNumber idNumber = (AlbanianIdNumber) map.computeIfAbsent(AlbanianIdNumber.class, aClass -> new AlbanianIdNumber()); - return idNumber.getInvalid(faker); + return provider(AlbanianIdNumber.class).getInvalid(faker); } /** @@ -223,8 +210,7 @@ public String invalidAlbanianPersonalCode() { * @return A valid ID Number */ public String validMoldovaPersonalCode() { - MoldovaIdNumber idNumber = (MoldovaIdNumber) map.computeIfAbsent(MoldovaIdNumber.class, aClass -> new MoldovaIdNumber()); - return idNumber.getValid(faker); + return provider(MoldovaIdNumber.class).getValid(faker); } /** @@ -233,8 +219,37 @@ public String validMoldovaPersonalCode() { * @return An invalid ID Number */ public String invalidMoldovaPersonalCode() { - MoldovaIdNumber idNumber = (MoldovaIdNumber) map.computeIfAbsent(MoldovaIdNumber.class, aClass -> new MoldovaIdNumber()); - return idNumber.getInvalid(faker); + return provider(MoldovaIdNumber.class).getInvalid(faker); } + /** + * Generates a valid ID number for Bulgaria citizens and residents + * Specified as #{IDNumber.valid_bulgarian_personal_code} in et.yml + * @return A valid ID Number + */ + public String validBulgarianPersonalCode() { + return provider(BulgarianIdNumber.class).getValid(faker); + } + + /** + * Generates a valid ID number for Bulgaria citizens and residents + * Specified as #{IDNumber.invalid_bulgarian_personal_code} in et.yml + * @return An invalid ID Number + */ + public String invalidBulgarianPersonalCode() { + return provider(BulgarianIdNumber.class).getInvalid(faker); + } + + @SuppressWarnings("unchecked") + private T provider(Class clazz) { + return (T) providers.computeIfAbsent(clazz, aClass -> create(clazz)); + } + + private T create(Class clazz) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Failed to instantiate class " + clazz.getName(), e); + } + } } diff --git a/src/main/resources/bg.yml b/src/main/resources/bg.yml index 463f9e7d0..6f604a1cb 100644 --- a/src/main/resources/bg.yml +++ b/src/main/resources/bg.yml @@ -52,3 +52,7 @@ bg: passport: valid: "[0-9]{9}" + + id_number: + valid: "#{IDNumber.valid_bulgarian_personal_code}" + invalid: "#{IDNumber.invalid_bulgarian_personal_code}" diff --git a/src/test/java/net/datafaker/idnumbers/BulgarianIdNumberTest.java b/src/test/java/net/datafaker/idnumbers/BulgarianIdNumberTest.java new file mode 100644 index 000000000..6a4502fbb --- /dev/null +++ b/src/test/java/net/datafaker/idnumbers/BulgarianIdNumberTest.java @@ -0,0 +1,21 @@ +package net.datafaker.idnumbers; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class BulgarianIdNumberTest { + private final BulgarianIdNumber generator = new BulgarianIdNumber(); + + @Test + void checksum() { + assertThat(generator.checksum("803205603")).isEqualTo(1); + assertThat(generator.checksum("800101000")).isEqualTo(8); + assertThat(generator.checksum("750102001")).isEqualTo(8); + assertThat(generator.checksum("820630876")).isEqualTo(0); + assertThat(generator.checksum("560628204")).isEqualTo(7); + assertThat(generator.checksum("752316926")).isEqualTo(3); + assertThat(generator.checksum("755201000")).isEqualTo(5); + assertThat(generator.checksum("754201103")).isEqualTo(0); + } +} diff --git a/src/test/java/net/datafaker/providers/base/IdNumberTest.java b/src/test/java/net/datafaker/providers/base/IdNumberTest.java index 1d6339c65..80debb368 100644 --- a/src/test/java/net/datafaker/providers/base/IdNumberTest.java +++ b/src/test/java/net/datafaker/providers/base/IdNumberTest.java @@ -3,6 +3,7 @@ import net.datafaker.Faker; import net.datafaker.idnumbers.EnZAIdNumber; import net.datafaker.idnumbers.SvSEIdNumber; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; @@ -14,10 +15,14 @@ class IdNumberTest extends BaseFakerTest { - public static final IdNumber SV_SE_ID_NUMBER = new BaseFaker(new Locale("sv_SE")).idNumber(); - public static final Pattern SV_SE_ID_NUMBER_PATTERN = Pattern.compile("\\d{6}[-+]\\d{4}"); - public static final IdNumber EN_ZA_OD_NUMBER= new BaseFaker(new Locale("en_ZA")).idNumber(); - public static final Pattern EN_ZA_ID_NUMBER_PATTERN = Pattern.compile("[0-9]{10}([01])8[0-9]"); + private static final IdNumber SV_SE_ID_NUMBER = new BaseFaker(new Locale("sv_SE")).idNumber(); + private static final Pattern SV_SE_ID_NUMBER_PATTERN = Pattern.compile("\\d{6}[-+]\\d{4}"); + private static final IdNumber EN_ZA_OD_NUMBER= new BaseFaker(new Locale("en_ZA")).idNumber(); + private static final Pattern EN_ZA_ID_NUMBER_PATTERN = Pattern.compile("[0-9]{10}([01])8[0-9]"); + private static final Faker ESTONIAN = new Faker(new Locale("et", "EE")); + private static final Faker ALBANIAN = new Faker(new Locale("sq", "AL")); + private static final Faker MOLDOVAN = new Faker(new Locale("ro", "MD")); + private static final Faker BULGARIAN = new Faker(new Locale("bg", "BG")); @Test void testValid() { @@ -85,21 +90,18 @@ void testPeselNumber() { @RepeatedTest(100) void estonianPersonalCode_valid() { - Faker faker = new Faker(new Locale("et", "EE")); - assertThat(faker.idNumber().valid()).matches("[1-6][0-9]{10}"); + assertThatPin(ESTONIAN.idNumber().valid()).matches("[1-6][0-9]{10}"); } @RepeatedTest(100) void estonianPersonalCode_invalid() { - Faker faker = new Faker(new Locale("et", "EE")); - assertThat(faker.idNumber().invalid()).matches("[1-6][0-9]{10}"); + assertThatPin(ESTONIAN.idNumber().invalid()).matches("[1-6][0-9]{10}"); } @RepeatedTest(100) void albanianPersonalCode_valid() { - Faker faker = new Faker(new Locale("sq", "AL")); - String pin = faker.idNumber().valid(); - assertThat(pin.length()).isEqualTo(10); + String pin = ALBANIAN.idNumber().valid(); + assertThatPin(pin).matches("\\w\\d{8}\\w"); assertThat(parseInt(pin.substring(2, 4)) % 50) .as(() -> "Valid PIN %s should have month number between 1..12 (for males) or 51..62 (for females)".formatted(pin)) .isBetween(1, 12); @@ -107,9 +109,8 @@ void albanianPersonalCode_valid() { @RepeatedTest(100) void albanianPersonalCode_invalid() { - Faker faker = new Faker(new Locale("sq", "AL")); - String pin = faker.idNumber().invalid(); - assertThat(pin.length()).isEqualTo(10); + String pin = ALBANIAN.idNumber().invalid(); + assertThatPin(pin).matches("\\w\\d{8}\\w"); assertThat(parseInt(pin.substring(2, 4))) .as(() -> "Invalid PIN %s should have month greater than (any month + 50)".formatted(pin)) .isGreaterThan(62); @@ -117,19 +118,30 @@ void albanianPersonalCode_invalid() { @RepeatedTest(100) void moldovaPersonalCode_valid() { - Faker faker = new Faker(new Locale("ro", "MD")); - String pin = faker.idNumber().valid(); - assertThat(pin.length()) - .as(() -> "Presumably valid PIN: '%s'".formatted(pin)) - .isEqualTo(13); + String pin = MOLDOVAN.idNumber().valid(); + assertThatPin(pin).matches("\\d{13}"); } @RepeatedTest(100) void moldovaPersonalCode_invalid() { - Faker faker = new Faker(new Locale("ro", "MD")); - String pin = faker.idNumber().invalid(); - assertThat(pin.length()) - .as(() -> "Presumably invalid PIN: '%s'".formatted(pin)) - .isEqualTo(13); + String pin = MOLDOVAN.idNumber().invalid(); + assertThatPin(pin).matches("\\d{13}"); + } + + @RepeatedTest(100) + void bulgarianPersonalCode_valid() { + String pin = BULGARIAN.idNumber().valid(); + assertThatPin(pin).matches("\\d{10}"); + } + + @RepeatedTest(100) + void bulgarianPersonalCode_invalid() { + String pin = BULGARIAN.idNumber().invalid(); + assertThatPin(pin).matches("\\d{10}"); + } + + private static AbstractStringAssert assertThatPin(String pin) { + return assertThat(pin) + .as(() -> "PIN: %s".formatted(pin)); } } diff --git a/src/test/java/net/datafaker/providers/base/PhoneNumberTest.java b/src/test/java/net/datafaker/providers/base/PhoneNumberTest.java index b35597bf2..68cf3e74b 100644 --- a/src/test/java/net/datafaker/providers/base/PhoneNumberTest.java +++ b/src/test/java/net/datafaker/providers/base/PhoneNumberTest.java @@ -3,6 +3,8 @@ import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; +import net.datafaker.Faker; +import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -16,6 +18,8 @@ class PhoneNumberTest extends BaseFakerTest { + private static final Faker ESTONIAN = new Faker(new Locale("et", "EE")); + private static final Faker MOLDOVAN = new Faker(new Locale("ro", "MD")); @Test void testCellPhone_enUS() { @@ -100,6 +104,7 @@ private static Stream generateLanguageAndRegionOfLocales() { Arguments.of(new Locale("en", "MS"), "MS"), Arguments.of(new Locale("en", "NG"), "NG"), Arguments.of(new Locale("en", "NZ"), "NZ"), + Arguments.of(new Locale("et", "EE"), "EE"), Arguments.of(new Locale("bg", "BG"), "BG"), Arguments.of(new Locale("by", "BY"), "BY"), Arguments.of(new Locale("ca", "CA"), "CA"), @@ -119,6 +124,7 @@ private static Stream generateLanguageAndRegionOfLocales() { Arguments.of(new Locale("nl", "NL"), "NL"), Arguments.of(new Locale("pl", "PL"), "PL"), Arguments.of(new Locale("pt", "PT"), "PT"), + Arguments.of(new Locale("ro", "MD"), "MD"), Arguments.of(new Locale("zh", "CN"), "CN"), Arguments.of(new Locale("zh", "TW"), "TW"), Arguments.of(new Locale("uk", "UA"), "UA"), @@ -178,16 +184,36 @@ void testSubscriberNumberWithLength() { @RepeatedTest(10) void cellPhone_estonia() { - BaseFaker f = new BaseFaker(new Locale("et", "EE")); - String cellPhone = f.phoneNumber().cellPhone(); - assertThat(cellPhone).matches("[58]\\d{2,3} \\d{4}"); + String cellPhone = ESTONIAN.phoneNumber().cellPhone(); + assertThatPhone(cellPhone).matches("[58]\\d{2,3} \\d{4}"); } @RepeatedTest(10) void phoneNumberInternational_estonia() { - BaseFaker f = new BaseFaker(new Locale("et", "EE")); - String cellPhone = f.phoneNumber().phoneNumberInternational(); - assertThat(cellPhone).matches("\\+372 5\\d \\d{2} \\d{2} \\d{2}"); + String cellPhone = ESTONIAN.phoneNumber().phoneNumberInternational(); + assertThatPhone(cellPhone).matches("\\+372 5\\d \\d{2} \\d{2} \\d{2}"); + } + + @RepeatedTest(10) + void cellPhone_moldova() { + String phone = MOLDOVAN.phoneNumber().cellPhone().replaceAll("\\s", ""); + assertThatPhone(phone).matches("0[567]\\d{7}"); } + @RepeatedTest(10) + void phoneNumber_moldova() { + String phone = MOLDOVAN.phoneNumber().phoneNumber().replaceAll("\\s+", ""); + assertThatPhone(phone).matches("0\\d{8}"); + } + + @RepeatedTest(10) + void phoneNumberInternational_moldova() { + String phone = MOLDOVAN.phoneNumber().phoneNumberInternational().replaceAll("\\s+", ""); + assertThatPhone(phone).matches("\\+373\\d{8}"); + } + + private static AbstractStringAssert assertThatPhone(String phoneNumber) { + return assertThat(phoneNumber) + .as(() -> "Phone: %s".formatted(phoneNumber)); + } }