From e4dc2daa4a9a780a6dfb898498008b46b7f0046e Mon Sep 17 00:00:00 2001 From: Marc Wrobel Date: Sun, 28 May 2023 17:04:06 +0200 Subject: [PATCH] Add random BIC generation (closes #338) --- CHANGELOG.md | 2 + README.md | 10 +- .../fr/marcwrobel/jbanking/bic/RandomBic.java | 115 ++++++++++++++++++ .../jbanking/bic/RandomBicTest.java | 108 ++++++++++++++++ 4 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 src/main/java/fr/marcwrobel/jbanking/bic/RandomBic.java create mode 100644 src/test/java/fr/marcwrobel/jbanking/bic/RandomBicTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c2373cd..fcc32bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Support random IBAN generation based on countries (`RandomIban.next(IsoCountry...)`), country alpha-2 codes (`RandomIban.next(String...)`) or currencies (`RandomIban.next(IsoCurrency...)`) (#339). +- Support random BIC generation based on countries (`RandomBic.next(IsoCountry...)`) or country + alpha-2 codes (`RandomBic.next(String...)`) (#338). ### Changed diff --git a/README.md b/README.md index f138b03..9ab1e18 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,11 @@ jbanking is supporting the following features : * [Single Euro Payments Area (SEPA)](https://wikipedia.org/wiki/Single_Euro_Payments_Area). * [ISO 4217 currencies](https://wikipedia.org/wiki/ISO_4217) (with alphabetic code, numeric code, minor unit and countries using it). -* [ISO 9362:2009 BIC](https://wikipedia.org/wiki/Bank_Identifier_Code) handling and validation. -* [ISO 13616:2007 IBAN](https://wikipedia.org/wiki/International_Bank_Account_Number) handling and validation (for both +* [ISO 9362 BIC](https://wikipedia.org/wiki/Bank_Identifier_Code) handling and validation. +* [ISO 13616 IBAN](https://wikipedia.org/wiki/International_Bank_Account_Number) handling and validation (for both check digit and national bank account number structure). -* Random [ISO 13616:2007 IBAN](https://wikipedia.org/wiki/International_Bank_Account_Number) generation. +* Random [ISO 9362 BIC](https://wikipedia.org/wiki/Bank_Identifier_Code) and + [ISO 13616 IBAN](https://wikipedia.org/wiki/International_Bank_Account_Number) generation. * [Creditor Identifiers (CIs)](https://www.europeanpaymentscouncil.eu/document-library/guidance-documents/creditor-identifier-overview) handling and validation. * Configurable [holiday](https://wikipedia.org/wiki/Holiday) calendar support with predefined calendars for : @@ -109,6 +110,9 @@ Assertions.assertEquals("PP", bic.getLocationCode()); Assertions.assertEquals("XXX", bic.getBranchCode()); Assertions.assertTrue(bic.isLiveBic()); +// Generate a random BIC +Bic randomBic = new RandomBic().next(); + // Validate a creditor identifier Assertions.assertTrue(CreditorIdentifier.isValid(" fr72zzz123456 ")); diff --git a/src/main/java/fr/marcwrobel/jbanking/bic/RandomBic.java b/src/main/java/fr/marcwrobel/jbanking/bic/RandomBic.java new file mode 100644 index 0000000..16da44b --- /dev/null +++ b/src/main/java/fr/marcwrobel/jbanking/bic/RandomBic.java @@ -0,0 +1,115 @@ +package fr.marcwrobel.jbanking.bic; + +import static fr.marcwrobel.jbanking.swift.SwiftPatternCharacterRepresentation.DIGITS; +import static fr.marcwrobel.jbanking.swift.SwiftPatternCharacterRepresentation.UPPER_CASE_LETTERS; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +import fr.marcwrobel.jbanking.IsoCountry; +import java.util.Random; + +/** + * Generates pseudorandom {@link Bic BICs}. + * + *

+ * Usage: + * + *

+ * // Generating a random BIC
+ * Bic random1 = new RandomBic().next();
+ *
+ * // Generating a random BIC using a given Random (in order to make the generation deterministic)
+ * Bic random2 = new RandomBic(new Random(0)).next();
+ *
+ * // Generating a random french or german BIC
+ * Bic random3 = new RandomBic().next(IsoCountry.FR, IsoCountry.DE);
+ * 
+ * + *

+ * This class should only be used for tests. + * + * @since 4.2.0 + */ +public class RandomBic { + + private static final String LETTERS = UPPER_CASE_LETTERS.alphabet(); + private static final String LETTERS_AND_DIGITS = LETTERS + DIGITS.alphabet(); + + private final Random random; + + /** + * Creates a new random BIC generator using the given {@link Random random number generator}. + * + * @param random a non-null {@link Random} instance + * @throws NullPointerException if the given {@link Random} instance is {@code null} + */ + public RandomBic(Random random) { + this.random = requireNonNull(random); + } + + /** + * Creates a new random BIC generator. + * + *

+ * This constructor is creating a new {@link Random random number generator} each time it is + * invoked. + */ + public RandomBic() { + // Note that Random was chosen over SecureRandom because security does not matter in our case and because Random : + // - produces the same result on all platforms, + // - produces the same results for a seed by default, + // - is random enough, + // - and is much faster. + this(new Random()); + } + + public Bic next() { + return next(IsoCountry.values()); + } + + /** + * Generates a random BIC for one of the given {@link IsoCountry country} (randomly chosen). + * + * @param countries a non-null and non-empty array of {@link IsoCountry} + * @return a non-null {@link Bic} + * @throws NullPointerException if {@code countries} is null + * @throws IllegalArgumentException if {@code countries} is empty + */ + public Bic next(IsoCountry... countries) { + IsoCountry country = countries[random.nextInt(countries.length)]; + return generate(country); + } + + /** + * Generates a random BIC for one of the given ISO country alpha-2 codes (randomly chosen). + * + * @param isoCountryAlpha2Codes a non-null and non-empty array of ISO country alpha-2 codes + * @return a non-null {@link Bic} + * @throws IllegalArgumentException if {@code isoCountryAlpha2Codes} is empty or if no corresponding {@link IsoCountry} can be + * found for the chosen ISO country alpha-2 code + */ + public Bic next(String... isoCountryAlpha2Codes) { + String countryCode = isoCountryAlpha2Codes[random.nextInt(isoCountryAlpha2Codes.length)]; + + IsoCountry country = IsoCountry.fromAlpha2Code(countryCode).orElseThrow(() -> new IllegalArgumentException( + format("no corresponding country could be found for alpha-2 code '%s'", countryCode))); + + return generate(country); + } + + private Bic generate(IsoCountry country) { + StringBuilder bic = new StringBuilder(Bic.BIC11_LENGTH); + + for (int i = 0; i < Bic.INSTITUTION_CODE_LENGTH; i++) { + bic.append(LETTERS.charAt(random.nextInt(LETTERS.length()))); + } + + bic.append(country.getAlpha2Code()); + + for (int i = 0; i < Bic.LOCATION_CODE_LENGTH + Bic.BRANCH_CODE_LENGTH; i++) { + bic.append(LETTERS_AND_DIGITS.charAt(random.nextInt(LETTERS_AND_DIGITS.length()))); + } + + return new Bic(bic.toString()); + } +} diff --git a/src/test/java/fr/marcwrobel/jbanking/bic/RandomBicTest.java b/src/test/java/fr/marcwrobel/jbanking/bic/RandomBicTest.java new file mode 100644 index 0000000..a3403fd --- /dev/null +++ b/src/test/java/fr/marcwrobel/jbanking/bic/RandomBicTest.java @@ -0,0 +1,108 @@ +package fr.marcwrobel.jbanking.bic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import fr.marcwrobel.jbanking.IsoCountry; +import fr.marcwrobel.jbanking.iban.BbanStructure; +import java.util.Random; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class RandomBicTest { + + @Test + void whenRandomIsNull_thenThrows() { + assertThrows(NullPointerException.class, () -> new RandomBic(null)); + } + + @Test + void whenRandomIsKnown_thenResultIsDeterministic() { + RandomBic random = new RandomBic(new Random(0)); + Bic bic = random.next(); + assertEquals(new Bic("SXVNKW39VPC"), bic); + } + + @Test + void generatedBicsAreValid() { + RandomBic random = new RandomBic(); + + for (int i = 0; i < BbanStructure.values().length * 10000; i++) { + Bic bic = random.next(); + assertTrue(Bic.isValid(bic.toString())); + } + } + + @Nested + class ByCountry { + + @Test + void whenArrayIsNull_thenThrows() { + RandomBic random = new RandomBic(); + assertThrows(NullPointerException.class, () -> random.next((IsoCountry[]) null)); + } + + @Test + void whenArrayContainsNull_thenThrows() { + RandomBic random = new RandomBic(); + assertThrows(NullPointerException.class, () -> random.next(new IsoCountry[] { null })); + } + + @Test + void whenArrayIsEmpty_thenThrows() { + RandomBic random = new RandomBic(); + assertThrows(IllegalArgumentException.class, () -> random.next(new IsoCountry[] {})); + } + + @Test + void whenRandomIsKnown_thenResultIsDeterministic() { + RandomBic random = new RandomBic(new Random(0)); + Bic bic = random.next(IsoCountry.FR, IsoCountry.DE); + assertEquals(new Bic("SXVNDE39VPC"), bic); + } + + @Test + void whenStructureIsKnown_thenSameIbanCountry() { + RandomBic random = new RandomBic(); + Bic bic = random.next(IsoCountry.FR); + assertEquals(IsoCountry.FR, bic.getCountry()); + } + } + + @Nested + class ByCountryCode { + + @Test + void whenArrayIsNull_thenThrows() { + RandomBic random = new RandomBic(); + assertThrows(NullPointerException.class, () -> random.next((String[]) null)); + } + + @Test + void whenArrayContainsNull_thenThrows() { + RandomBic random = new RandomBic(); + assertThrows(IllegalArgumentException.class, () -> random.next(new String[] { null })); + } + + @Test + void whenArrayIsEmpty_thenThrows() { + RandomBic random = new RandomBic(); + assertThrows(IllegalArgumentException.class, () -> random.next(new String[] {})); + } + + @Test + void whenRandomIsKnown_thenResultIsDeterministic() { + RandomBic random = new RandomBic(new Random(0)); + Bic bic = random.next(IsoCountry.JP.getAlpha2Code(), IsoCountry.US.getAlpha2Code()); + assertEquals(new Bic("SXVNUS39VPC"), bic); + } + + @Test + void whenStructureIsKnown_thenSameIbanCountry() { + RandomBic random = new RandomBic(); + Bic bic = random.next(IsoCountry.GB.getAlpha2Code()); + assertEquals(IsoCountry.GB, bic.getCountry()); + } + } +}