Skip to content

Commit

Permalink
Add random BIC generation (closes #338)
Browse files Browse the repository at this point in the history
RandomBic is heavily inspired by RandomIban and provide nearly the same set of methods.
  • Loading branch information
marcwrobel committed May 28, 2023
1 parent 4ff47b2 commit 3d53b1c
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...)`), country alpha-2
codes (`RandomBic.next(String...)`) or currencies (`RandomBic.next(IsoCurrency...)`) (#338).

### Changed

Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down Expand Up @@ -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 "));

Expand Down
134 changes: 134 additions & 0 deletions src/main/java/fr/marcwrobel/jbanking/bic/RandomBic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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 fr.marcwrobel.jbanking.IsoCurrency;
import java.util.Arrays;
import java.util.Random;

/**
* Generates pseudorandom {@link Bic BICs}.
*
* <p>
* Usage:
*
* <pre>
* // 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);
* </pre>
*
* <p>
* 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.
*
* <p>
* 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);
}

/**
* Generates a random BIC for one of the given {@link IsoCurrency currency} (randomly chosen).
* <br>
* This method is not efficient: it needs to build a sorted array of all currencies' countries each time it is invoked.
*
* @param currencies a non-null and non-empty array of {@link IsoCurrency}
* @return a non-null {@link Bic}
* @throws IllegalArgumentException if {@code currencies} is empty.
*/
public Bic next(IsoCurrency... currencies) {
IsoCountry[] countries = Arrays.stream(currencies)
.flatMap(currency -> currency.getCountries().stream())
.sorted()
.toArray(IsoCountry[]::new);
return next(countries);
}

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());
}
}
144 changes: 144 additions & 0 deletions src/test/java/fr/marcwrobel/jbanking/bic/RandomBicTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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.IsoCurrency;
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 < IsoCountry.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 whenCountryIsKnown_thenSameBicCountry() {
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 whenCountryIsKnown_thenSameBicCountry() {
RandomBic random = new RandomBic();
Bic bic = random.next(IsoCountry.GB.getAlpha2Code());
assertEquals(IsoCountry.GB, bic.getCountry());
}
}

@Nested
class ByCurrency {

@Test
void whenArrayIsNull_thenThrows() {
RandomBic random = new RandomBic();
assertThrows(NullPointerException.class, () -> random.next((IsoCurrency[]) null));
}

@Test
void whenArrayContainsNull_thenThrows() {
RandomBic random = new RandomBic();
assertThrows(NullPointerException.class, () -> random.next(new IsoCurrency[] { null }));
}

@Test
void whenArrayIsEmpty_thenThrows() {
RandomBic random = new RandomBic();
assertThrows(IllegalArgumentException.class, () -> random.next(new IsoCurrency[] {}));
}

@Test
void whenRandomIsKnown_thenResultIsDeterministic() {
RandomBic random = new RandomBic(new Random(0));
Bic bic = random.next(IsoCurrency.EUR, IsoCurrency.USD);
assertEquals(new Bic("SXVNDE39VPC"), bic);
}

@Test
void whenCountryIsKnown_thenSameBicCountry() {
RandomBic random = new RandomBic();
Bic bic = random.next(IsoCurrency.JPY);
assertEquals(IsoCountry.JP, bic.getCountry());
}
}
}

0 comments on commit 3d53b1c

Please sign in to comment.