diff --git a/.gitignore b/.gitignore index 80050fd..c6fc549 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .project .settings/ .factorypath +.envrc target/ @@ -32,4 +33,4 @@ hs_err_pid* /.apt_generated_tests/ .idea/ -*.iml \ No newline at end of file +*.iml diff --git a/pom.xml b/pom.xml index 224d7b2..debd9d8 100644 --- a/pom.xml +++ b/pom.xml @@ -140,6 +140,11 @@ nexus-staging-maven-plugin 1.6.7 + + org.owasp + dependency-check-maven + 8.4.0 + @@ -174,10 +179,18 @@ true - - + + org.owasp + dependency-check-maven + + + + check + + + + - @@ -187,26 +200,6 @@ 1.18.10 provided - - org.apache.commons - commons-text - 1.10.0 - - - org.apache.commons - commons-lang3 - 3.9 - - - com.google.guava - guava - 32.1.1-jre - - - org.apache.commons - commons-csv - 1.4 - junit junit diff --git a/src/main/java/it/kamaladafrica/codicefiscale/CodiceFiscale.java b/src/main/java/it/kamaladafrica/codicefiscale/CodiceFiscale.java index b71cb79..6342540 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/CodiceFiscale.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/CodiceFiscale.java @@ -1,14 +1,9 @@ package it.kamaladafrica.codicefiscale; -import static org.apache.commons.lang3.Validate.inclusiveBetween; -import static org.apache.commons.lang3.Validate.matchesPattern; - import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; -import org.apache.commons.lang3.Validate; - import it.kamaladafrica.codicefiscale.city.CityByBelfiore; import it.kamaladafrica.codicefiscale.city.CityProvider; import it.kamaladafrica.codicefiscale.internal.BelfiorePart; @@ -96,8 +91,10 @@ public boolean isEqual(CodiceFiscale other, boolean ignoreOmocode) { public CodiceFiscale toOmocodeLevel(int level) { if ((level & OMOCODE_LEVEL_MASK) != 0) { - inclusiveBetween(0, OMOCODE_LEVEL_MASK, level, "invalid omocode level: 0 <= %s <= %s", level, - OMOCODE_LEVEL_MASK); + if (level < 0 || level > OMOCODE_LEVEL_MASK) { + throw new IllegalArgumentException( + String.format("invalid omocode level: 0 <= %s <= %s", level, OMOCODE_LEVEL_MASK)); + } } DatePart datePart = getDate().toOmocodeLevel(level & OMOCODE_LEVEL_DATE_MASK); BelfiorePart belfiorePart = getBelfiore().toOmocodeLevel(level & OMOCODE_LEVEL_BELFIORE_MASK); @@ -158,13 +155,7 @@ public static CodiceFiscale of(String value, CityByBelfiore provider) { final int level = (date.getOmocodeLevel().getLevel() << OMOCODE_LEVEL_DATE_OFFSET) | (belfiore.getOmocodeLevel().getLevel() << OMOCODE_LEVEL_BELFIORE_OFFSET); - final CodiceFiscale result = new CodiceFiscale(person, lastname, firstname, date, belfiore, level); - - Validate.isTrue(Objects.equals(result.getValue(), value), "expected %s, but found %s", value, - result.getValue()); - - return result; - + return new CodiceFiscale(person, lastname, firstname, date, belfiore, level); } public static boolean isCompatible(String code, Person person) { @@ -181,11 +172,17 @@ public static boolean isFormatValid(String value) { } public static String validate(String value) { - matchesPattern(value, VALIDATION_PATTERN); + if (!value.matches(VALIDATION_PATTERN)) { + throw new IllegalArgumentException( + String.format("The string %s does not match the pattern %s", value, VALIDATION_PATTERN)); + } final ControlPart control = ControlPart.of(value.substring(LASTNAME_PART_INDEX, CONTROL_PART_INDEX)); final char currentControl = value.charAt(CONTROL_PART_INDEX); - Validate.isTrue(control.isEqual(currentControl), "invalid control char: expected %s, but found %s", - control.getValue(), currentControl); + + if (!control.isEqual(currentControl)) { + throw new IllegalArgumentException(String.format("invalid control char: expected %s, but found %s", + control.getValue(), currentControl)); + } return value; } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/CityStreamSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/CityStreamSupplier.java new file mode 100644 index 0000000..dd8aff0 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/CityStreamSupplier.java @@ -0,0 +1,10 @@ +package it.kamaladafrica.codicefiscale.city; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import it.kamaladafrica.codicefiscale.City; + +public interface CityStreamSupplier extends Supplier> { + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritm.java b/src/main/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritm.java new file mode 100644 index 0000000..8e0e196 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritm.java @@ -0,0 +1,85 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +import java.util.Objects; + +/** + * A similarity algorithm that returns 1.0 only if left and right matches, 0.0 + * otherwise. + * + * By default computation ignore the case,so "AAA" and "aaa" are equals + */ +public class ExactMatchAlgoritm implements ScoreAlgoritm { + + public static final double MATCH_SCORE = 1.0; + public static final double NO_MATCH_SCORE = 0.0; + + private final boolean ignoreCase; + + public ExactMatchAlgoritm(boolean ignoreCase) { + this.ignoreCase = ignoreCase; + } + + public ExactMatchAlgoritm() { + this(true); + } + + /** + * Computes the Exact Match Similarity between two character sequences. + * + *
+	 * sim.apply(null, null)          = IllegalArgumentException
+	 * sim.apply("foo", null)         = IllegalArgumentException
+	 * sim.apply(null, "foo")         = IllegalArgumentException
+	 * sim.apply("", "")              = 1.0
+	 * sim.apply("foo", "foo")        = 1.0
+	 * sim.apply("foo", "foo ")       = 0.0
+	 * sim.apply("", "a")             = 0.0
+	 * sim.apply("frog", "fog")       = 0.0
+	 * sim.apply("fly", "ant")        = 0.0
+	 * sim.apply("fly", "FLY")        = 1.0 if ignoreCase is true, 0.0 otherwise
+	 * sim.apply("fly", "fLy")        = 1.0 if ignoreCase is true, 0.0 otherwise
+	 * 
+ * + * @param left the first CharSequence, must not be null + * @param right the second CharSequence, must not be null + * @return result similarity + * @throws IllegalArgumentException if either CharSequence input is {@code null} + */ + @Override + public Double apply(final CharSequence left, final CharSequence right) { + if (left == null || right == null) { + throw new IllegalArgumentException("CharSequences must not be null"); + } + return toScore(areEquals(left, right, ignoreCase)); + } + + private static boolean areEquals(final CharSequence left, final CharSequence right, boolean ignoreCase) { + if (Objects.equals(left, right)) { + return true; + } + + if (left.length() != right.length()) { + return false; + } + + // Step-wise comparison + final int length = left.length(); + for (int i = 0; i < length; i++) { + char lc = left.charAt(i); + char rc = right.charAt(i); + if (ignoreCase && (Character.isLowerCase(lc) != Character.isLowerCase(rc))) { + lc = Character.toLowerCase(lc); + rc = Character.toLowerCase(rc); + } + if (lc != rc) { + return false; + } + } + return true; + } + + private static double toScore(boolean match) { + return match ? MATCH_SCORE : NO_MATCH_SCORE; + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritm.java b/src/main/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritm.java new file mode 100644 index 0000000..33921cc --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritm.java @@ -0,0 +1,148 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +import java.util.Arrays; + +import it.kamaladafrica.codicefiscale.utils.StringUtils; + +/** + * A similarity algorithm indicating the percentage of matched characters + * between two character sequences. + * + *

+ * The Jaro measure is the weighted sum of percentage of matched characters from + * each file and transposed characters. Winkler increased this measure for + * matching initial characters. + *

+ * + *

+ * This implementation is based on the Jaro Winkler similarity algorithm from + * + * http://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance. + *

+ * + *

+ * This code has been adapted from Apache Commons Lang 3.3. + *

+ * + * @since 1.7 + */ +public class JaroWinklerAlgoritm implements ScoreAlgoritm { + + /** + * This method returns the Jaro-Winkler string matches, half transpositions, + * prefix array. + * + * @param first the first string to be matched + * @param second the second string to be matched + * @return mtp array containing: matches, half transpositions, and prefix + */ + protected static int[] matches(final CharSequence first, final CharSequence second) { + final CharSequence max; + final CharSequence min; + if (first.length() > second.length()) { + max = first; + min = second; + } else { + max = second; + min = first; + } + final int range = Math.max(max.length() / 2 - 1, 0); + final int[] matchIndexes = new int[min.length()]; + Arrays.fill(matchIndexes, -1); + final boolean[] matchFlags = new boolean[max.length()]; + int matches = 0; + for (int mi = 0; mi < min.length(); mi++) { + final char c1 = min.charAt(mi); + for (int xi = Math.max(mi - range, 0), xn = Math.min(mi + range + 1, max.length()); xi < xn; xi++) { + if (!matchFlags[xi] && c1 == max.charAt(xi)) { + matchIndexes[mi] = xi; + matchFlags[xi] = true; + matches++; + break; + } + } + } + final char[] ms1 = new char[matches]; + final char[] ms2 = new char[matches]; + for (int i = 0, si = 0; i < min.length(); i++) { + if (matchIndexes[i] != -1) { + ms1[si] = min.charAt(i); + si++; + } + } + for (int i = 0, si = 0; i < max.length(); i++) { + if (matchFlags[i]) { + ms2[si] = max.charAt(i); + si++; + } + } + int halfTranspositions = 0; + for (int mi = 0; mi < ms1.length; mi++) { + if (ms1[mi] != ms2[mi]) { + halfTranspositions++; + } + } + int prefix = 0; + for (int mi = 0; mi < Math.min(4, min.length()); mi++) { + if (first.charAt(mi) != second.charAt(mi)) { + break; + } + prefix++; + } + return new int[] { matches, halfTranspositions, prefix }; + } + + /** + * Computes the Jaro Winkler Similarity between two character sequences. + * + *
+	 * sim.apply(null, null)          = IllegalArgumentException
+	 * sim.apply("foo", null)         = IllegalArgumentException
+	 * sim.apply(null, "foo")         = IllegalArgumentException
+	 * sim.apply("", "")              = 1.0
+	 * sim.apply("foo", "foo")        = 1.0
+	 * sim.apply("foo", "foo ")       = 0.94
+	 * sim.apply("foo", "foo  ")      = 0.91
+	 * sim.apply("foo", " foo ")      = 0.87
+	 * sim.apply("foo", "  foo")      = 0.51
+	 * sim.apply("", "a")             = 0.0
+	 * sim.apply("aaapppp", "")       = 0.0
+	 * sim.apply("frog", "fog")       = 0.93
+	 * sim.apply("fly", "ant")        = 0.0
+	 * sim.apply("elephant", "hippo") = 0.44
+	 * sim.apply("hippo", "elephant") = 0.44
+	 * sim.apply("hippo", "zzzzzzzz") = 0.0
+	 * sim.apply("hello", "hallo")    = 0.88
+	 * sim.apply("ABC Corporation", "ABC Corp") = 0.91
+	 * sim.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.") = 0.95
+	 * sim.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness") = 0.92
+	 * sim.apply("PENNSYLVANIA", "PENNCISYLVNIA") = 0.88
+	 * 
+ * + * @param left the first CharSequence, must not be null + * @param right the second CharSequence, must not be null + * @return result similarity + * @throws IllegalArgumentException if either CharSequence input is {@code null} + */ + @Override + public Double apply(final CharSequence left, final CharSequence right) { + final double defaultScalingFactor = 0.1; + + if (left == null || right == null) { + throw new IllegalArgumentException("CharSequences must not be null"); + } + + if (StringUtils.equals(left, right)) { + return 1d; + } + + final int[] mtp = matches(left, right); + final double m = mtp[0]; + if (m == 0) { + return 0d; + } + final double j = (m / left.length() + m / right.length() + (m - (double) mtp[1] / 2) / m) / 3; + return j < 0.7d ? j : j + defaultScalingFactor * mtp[2] * (1d - j); + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/algo/ScoreAlgoritm.java b/src/main/java/it/kamaladafrica/codicefiscale/city/algo/ScoreAlgoritm.java new file mode 100644 index 0000000..f038093 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/algo/ScoreAlgoritm.java @@ -0,0 +1,47 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +/** + * Interface for the concept of a string similarity score. + * + *

+ * A string similarity score is intended to have some of the properties + * of a metric, yet allowing for exceptions, namely the Jaro-Winkler similarity + * score. + *

+ *

+ * We Define a SimilarityScore to be a function + * {@code d: [X * X] -> [0, INFINITY)} with the following properties: + *

+ *
    + *
  • {@code d(x,y) >= 0}, non-negativity or separation axiom
  • + *
  • {@code d(x,y) == d(y,x)}, symmetry.
  • + *
+ * + *

+ * Notice, these are two of the properties that contribute to d being a metric. + *

+ * + * + *

+ * Further, this intended to be BiFunction<CharSequence, CharSequence, R>. + * The {@code apply} method accepts a pair of {@link CharSequence} parameters + * and returns an {@code R} type similarity score. We have omitted the explicit + * statement of extending BiFunction due to it only being implemented in Java + * 1.8, and we wish to maintain Java 1.7 compatibility. + *

+ * + * @param The type of similarity score unit used by this EditDistance. + * @since 1.0 + */ +public interface ScoreAlgoritm { + + /** + * Compares two CharSequences. + * + * @param left the first CharSequence + * @param right the second CharSequence + * @return The similarity score between two CharSequences + */ + R apply(CharSequence left, CharSequence right); + +} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImpl.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImpl.java index baffccc..2947e7f 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImpl.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImpl.java @@ -1,46 +1,48 @@ package it.kamaladafrica.codicefiscale.city.impl; -import static com.google.common.base.Strings.nullToEmpty; -import static com.google.common.collect.ImmutableMap.toImmutableMap; import static java.util.function.Function.identity; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.text.similarity.JaroWinklerSimilarity; -import org.apache.commons.text.similarity.SimilarityScoreFrom; - -import com.google.common.collect.ImmutableList; import it.kamaladafrica.codicefiscale.City; import it.kamaladafrica.codicefiscale.CodiceFiscale; import it.kamaladafrica.codicefiscale.city.CityProvider; +import it.kamaladafrica.codicefiscale.city.algo.JaroWinklerAlgoritm; +import it.kamaladafrica.codicefiscale.city.algo.ScoreAlgoritm; +import it.kamaladafrica.codicefiscale.city.impl.csv.EsteriCsvSupplier; +import it.kamaladafrica.codicefiscale.city.impl.csv.ItaliaCsvSupplier; +import it.kamaladafrica.codicefiscale.utils.Pair; public final class CityProviderImpl implements CityProvider { public static final double DEFAULT_MINIMUM_MATCH_SCORE = 0.8; public static final double EXACT_MATCH_SCORE = 1.0; - static final String ITALIA_RESOURCE_PATH = "/italia.csv"; - static final String ESTERI_RESOURCE_PATH = "/esteri.csv"; - static final String ESTERI_CESSATI_RESOURCE_PATH = "/esteri-cessati.csv"; + static final String ITALIA_RESOURCE_PATH = "italia.csv"; + static final String ESTERI_RESOURCE_PATH = "esteri.csv"; + static final String ESTERI_CESSATI_RESOURCE_PATH = "esteri-cessati.csv"; private final Map cityByName; private final Map cityByBelfiore; + private final ScoreAlgoritm scoreAlgoritm; private final double minimumMatchScore; - private CityProviderImpl(Set cities, double minimumMatchScore) { + private CityProviderImpl(Set cities, ScoreAlgoritm scoreAlgoritm, double minimumMatchScore) { + this.scoreAlgoritm = Objects.requireNonNull(scoreAlgoritm); this.minimumMatchScore = minimumMatchScore; - this.cityByName = cities.stream().collect(toImmutableMap(CityProviderImpl::cityName, identity())); - this.cityByBelfiore = cities.stream().collect(toImmutableMap(City::getBelfiore, identity())); + this.cityByName = Collections + .unmodifiableMap(cities.stream().collect(Collectors.toMap(CityProviderImpl::cityName, identity()))); + this.cityByBelfiore = Collections + .unmodifiableMap(cities.stream().collect(Collectors.toMap(City::getBelfiore, identity()))); } private static String cityName(City city) { @@ -48,12 +50,16 @@ private static String cityName(City city) { } private static String normalize(String s) { - return nullToEmpty(s).toUpperCase(CodiceFiscale.LOCALE); + if (s == null || s.isEmpty()) { + return ""; + } + return s.toUpperCase(CodiceFiscale.LOCALE); } @Override public List findAll() { - return ImmutableList.sortedCopyOf((a, b) -> a.getName().compareTo(b.getName()), cityByName.values()); + return Collections.unmodifiableList( + cityByName.values().stream().sorted(Comparator.comparing(City::getName)).collect(Collectors.toList())); } @Override @@ -64,8 +70,8 @@ public City findByName(String name) { result = cityByName.get(term); if (minimumMatchScore != EXACT_MATCH_SCORE && result == null) { - final SimilarityScoreFrom score = new SimilarityScoreFrom<>(new JaroWinklerSimilarity(), term); - result = cityByName.entrySet().stream().map(e -> Pair.of(e.getValue(), score.apply(e.getKey()))) + result = cityByName.entrySet().stream() + .map(e -> Pair.of(e.getValue(), scoreAlgoritm.apply(term, e.getKey()))) .filter(e -> e.getValue() >= minimumMatchScore).max(Comparator.comparing(Entry::getValue)) .map(Entry::getKey).orElse(null); } @@ -96,7 +102,11 @@ public static CityProviderImpl of(Supplier> supplier, double minimumMa } public static CityProviderImpl of(Set cities, double minimumMatchScore) { - return new CityProviderImpl(cities, minimumMatchScore); + return of(cities, new JaroWinklerAlgoritm(), minimumMatchScore); + } + + public static CityProviderImpl of(Set cities, ScoreAlgoritm scoreAlgoritm, double minimumMatchScore) { + return new CityProviderImpl(cities, scoreAlgoritm, minimumMatchScore); } public static CityProviderImpl of(Supplier> supplier) { @@ -108,11 +118,11 @@ public static CityProviderImpl of(Set cities) { } private static Supplier> defaultSupplier() { - return () -> Stream - .of(ItaliaCsvSupplier.of(CityProviderImpl.class.getResource(ITALIA_RESOURCE_PATH)).get(), - EsteriCsvSupplier.of(CityProviderImpl.class.getResource(ESTERI_RESOURCE_PATH)).get(), - EsteriCsvSupplier.of(CityProviderImpl.class.getResource(ESTERI_CESSATI_RESOURCE_PATH)).get()) - .flatMap(identity()).collect(Collectors.toSet()); + return () -> CompositeCityStreamSupplier + .of(ItaliaCsvSupplier.of(CityProviderImpl.class.getResource(ITALIA_RESOURCE_PATH)), + EsteriCsvSupplier.of(CityProviderImpl.class.getResource(ESTERI_RESOURCE_PATH)), + EsteriCsvSupplier.of(CityProviderImpl.class.getResource(ESTERI_CESSATI_RESOURCE_PATH))) + .get().collect(Collectors.toSet()); } } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CompositeCityStreamSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CompositeCityStreamSupplier.java new file mode 100644 index 0000000..97edd0a --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CompositeCityStreamSupplier.java @@ -0,0 +1,29 @@ +package it.kamaladafrica.codicefiscale.city.impl; + +import java.util.stream.Stream; + +import it.kamaladafrica.codicefiscale.City; +import it.kamaladafrica.codicefiscale.city.CityStreamSupplier; +import lombok.Value; + +@Value(staticConstructor = "of") +public class CompositeCityStreamSupplier implements CityStreamSupplier { + + private final CityStreamSupplier[] suppliers; + + @Override + public Stream get() { + return Stream.of(suppliers).flatMap(CityStreamSupplier::get); + } + + public static CompositeCityStreamSupplier of(CityStreamSupplier first, CityStreamSupplier... others) { + int size = 1 + (others == null ? 0 : others.length); + CityStreamSupplier[] suppliers = new CityStreamSupplier[size]; + suppliers[0] = first; + if (others != null && others.length > 0) { + System.arraycopy(others, 0, suppliers, 1, others.length); + } + return of(suppliers); + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CsvSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CsvSupplier.java deleted file mode 100644 index 343926e..0000000 --- a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/CsvSupplier.java +++ /dev/null @@ -1,52 +0,0 @@ -package it.kamaladafrica.codicefiscale.city.impl; - -import static lombok.AccessLevel.PRIVATE; - -import java.io.IOException; -import java.net.URL; -import java.nio.charset.Charset; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVRecord; - -import it.kamaladafrica.codicefiscale.City; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - -@Getter(PRIVATE) -@RequiredArgsConstructor -public class CsvSupplier implements Supplier> { - - @NonNull - private final URL csvUrl; - @NonNull - private final Charset charset; - @NonNull - private final CSVFormat format; - @NonNull - private final Function mapper; - - @Override - public Stream get() { - try { - return streamRecords(parse()).map(mapper); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - } - - protected Stream streamRecords(CSVParser parser) { - return StreamSupport.stream(parser.spliterator(), false); - } - - protected CSVParser parse() throws IOException { - return CSVParser.parse(csvUrl, charset, format); - } - -} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplier.java deleted file mode 100644 index 2c07972..0000000 --- a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplier.java +++ /dev/null @@ -1,49 +0,0 @@ -package it.kamaladafrica.codicefiscale.city.impl; - -import static lombok.AccessLevel.PRIVATE; - -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.function.Function; -import java.util.stream.Stream; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVRecord; - -import it.kamaladafrica.codicefiscale.City; -import it.kamaladafrica.codicefiscale.CodiceFiscale; -import lombok.Getter; - -/** - * Reads record from istat csv file https://www.istat.it/it/files//2011/01/Elenco-codici-e-denominazioni-unita-territoriali-estere.zip - */ -@Getter(PRIVATE) -public final class EsteriCsvSupplier extends CsvSupplier { - - private EsteriCsvSupplier(URL resource) { - super(resource, StandardCharsets.UTF_8, buildFormat(), mapper()); - } - - private static Function mapper() { - return rec -> City.builder().name(rec.get(6).toUpperCase(CodiceFiscale.LOCALE)) - .prov(rec.get(2).toUpperCase(CodiceFiscale.LOCALE)) - .belfiore(rec.get(9).toUpperCase(CodiceFiscale.LOCALE)).build(); - } - - @Override - protected Stream streamRecords(CSVParser parser) { - // ignore Italy and n.d. - return super.streamRecords(parser).filter(rec -> rec.get(9).startsWith("Z")); - } - - private static CSVFormat buildFormat() { - return CSVFormat.DEFAULT.withDelimiter(';').withFirstRecordAsHeader().withTrim(); - } - - public static EsteriCsvSupplier of(URL url) { - return new EsteriCsvSupplier(url); - } - -} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplier.java deleted file mode 100644 index e0849a7..0000000 --- a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplier.java +++ /dev/null @@ -1,42 +0,0 @@ -package it.kamaladafrica.codicefiscale.city.impl; - -import static lombok.AccessLevel.PRIVATE; - -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.function.Function; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVRecord; - -import it.kamaladafrica.codicefiscale.City; -import it.kamaladafrica.codicefiscale.CodiceFiscale; -import lombok.Getter; - -/** - * Reads records from Anagrafe nazionale della popolazione residente csv file - * https://raw.githubusercontent.com/italia/anpr/master/src/archivi/ANPR_archivio_comuni.csv - */ -@Getter(PRIVATE) -public final class ItaliaCsvSupplier extends CsvSupplier { - - private ItaliaCsvSupplier(URL resource) { - super(resource, StandardCharsets.UTF_8, buildFormat(), mapper()); - } - - private static Function mapper() { - return rec -> City.builder().name(rec.get(1).toUpperCase(CodiceFiscale.LOCALE)) - .prov(rec.get(2).toUpperCase(CodiceFiscale.LOCALE)) - .belfiore(rec.get(0).toUpperCase(CodiceFiscale.LOCALE)).build(); - } - - private static CSVFormat buildFormat() { - return CSVFormat.DEFAULT.withDelimiter(',').withFirstRecordAsHeader().withTrim(); - } - - public static ItaliaCsvSupplier of(URL url) { - return new ItaliaCsvSupplier(url); - } - -} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/AbstractCsvStreamSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/AbstractCsvStreamSupplier.java new file mode 100644 index 0000000..1a088ab --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/AbstractCsvStreamSupplier.java @@ -0,0 +1,43 @@ +package it.kamaladafrica.codicefiscale.city.impl.csv; + +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; + +@Getter(AccessLevel.PRIVATE) +public abstract class AbstractCsvStreamSupplier implements Supplier> { + + private final URL csvUrl; + + private final SimpleCsvParser parser; + + private final Charset charset; + + protected AbstractCsvStreamSupplier(@NonNull URL csvUrl, char separator, char quotes, @NonNull Charset charset) { + parser = new SimpleCsvParser(separator, quotes); + this.charset = charset; + this.csvUrl = csvUrl; + } + + protected AbstractCsvStreamSupplier(@NonNull URL csvUrl, char separator) { + this(csvUrl, separator, SimpleCsvParser.DEFAULT_QUOTE_CHARACTER, StandardCharsets.UTF_8); + } + + @Override + public Stream get() { + return streamRecords().map(this::mapper); + } + + protected Stream streamRecords() { + return parser.parse(csvUrl, charset).skip(1); // skip headers + } + + protected abstract T mapper(String[] row); + +} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/CsvException.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/CsvException.java new file mode 100644 index 0000000..68f5e51 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/CsvException.java @@ -0,0 +1,27 @@ +package it.kamaladafrica.codicefiscale.city.impl.csv; + +public class CsvException extends RuntimeException { + + private static final long serialVersionUID = -4707593681808158601L; + + public CsvException() { + // call super + } + + public CsvException(String message) { + super(message); + } + + public CsvException(Throwable cause) { + super(cause); + } + + public CsvException(String message, Throwable cause) { + super(message, cause); + } + + public CsvException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/EsteriCsvSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/EsteriCsvSupplier.java new file mode 100644 index 0000000..500d77f --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/EsteriCsvSupplier.java @@ -0,0 +1,44 @@ +package it.kamaladafrica.codicefiscale.city.impl.csv; + +import static lombok.AccessLevel.PRIVATE; + +import java.net.URL; +import java.util.stream.Stream; + +import it.kamaladafrica.codicefiscale.City; +import it.kamaladafrica.codicefiscale.CodiceFiscale; +import it.kamaladafrica.codicefiscale.city.CityStreamSupplier; +import lombok.Getter; + +/** + * Reads record from istat csv file https://www.istat.it/it/files//2011/01/Elenco-codici-e-denominazioni-unita-territoriali-estere.zip + */ +@Getter(PRIVATE) +public final class EsteriCsvSupplier extends AbstractCsvStreamSupplier implements CityStreamSupplier { + + public static final char SEPARATOR = ';'; + + private EsteriCsvSupplier(URL resource) { + super(resource, SEPARATOR); + } + + @Override + protected City mapper(String[] row) { + return City.builder() + .name(row[6].toUpperCase(CodiceFiscale.LOCALE)) + .prov(row[2].toUpperCase(CodiceFiscale.LOCALE)) + .belfiore(row[9].toUpperCase(CodiceFiscale.LOCALE)) + .build(); + } + + @Override + protected Stream streamRecords() { + return super.streamRecords().filter(row -> row[9].startsWith("Z")); + } + + public static EsteriCsvSupplier of(URL url) { + return new EsteriCsvSupplier(url); + } + +} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/ItaliaCsvSupplier.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/ItaliaCsvSupplier.java new file mode 100644 index 0000000..d2b7e86 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/ItaliaCsvSupplier.java @@ -0,0 +1,39 @@ +package it.kamaladafrica.codicefiscale.city.impl.csv; + +import static lombok.AccessLevel.PRIVATE; + +import java.net.URL; + +import it.kamaladafrica.codicefiscale.City; +import it.kamaladafrica.codicefiscale.CodiceFiscale; +import it.kamaladafrica.codicefiscale.city.CityStreamSupplier; +import lombok.Getter; + +/** + * Reads records from Anagrafe nazionale della popolazione residente csv file + * https://raw.githubusercontent.com/italia/anpr/master/src/archivi/ANPR_archivio_comuni.csv + */ +@Getter(PRIVATE) +public final class ItaliaCsvSupplier extends AbstractCsvStreamSupplier implements CityStreamSupplier { + + public static final char SEPARATOR = ','; + + private ItaliaCsvSupplier(URL resource) { + super(resource, SEPARATOR); + } + + @Override + protected City mapper(String[] row) { + return City.builder() + .name(row[1].toUpperCase(CodiceFiscale.LOCALE)) + .prov(row[2].toUpperCase(CodiceFiscale.LOCALE)) + .belfiore(row[0].toUpperCase(CodiceFiscale.LOCALE)) + .build(); + } + + public static ItaliaCsvSupplier of(URL url) { + return new ItaliaCsvSupplier(url); + } + +} \ No newline at end of file diff --git a/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/SimpleCsvParser.java b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/SimpleCsvParser.java new file mode 100644 index 0000000..b492e05 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/city/impl/csv/SimpleCsvParser.java @@ -0,0 +1,371 @@ +package it.kamaladafrica.codicefiscale.city.impl.csv; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * A very simple CSV parser released under a commercial-friendly license. This + * just implements splitting a single line into fields. + * + * @author Glen Smith + * @author Rainer Pruy + */ +public class SimpleCsvParser { + + final char separator; + + final char quotechar; + + final char escape; + + final boolean strictQuotes; + + private String pending; + private boolean inField = false; + + final boolean ignoreLeadingWhiteSpace; + + final boolean ignoreQuotations; + + /** + * The default separator to use if none is supplied to the constructor. + */ + public static final char DEFAULT_SEPARATOR = ','; + + public static final int INITIAL_READ_SIZE = 128; + + /** + * The default quote character to use if none is supplied to the constructor. + */ + public static final char DEFAULT_QUOTE_CHARACTER = '"'; + + /** + * The default escape character to use if none is supplied to the constructor. + */ + public static final char DEFAULT_ESCAPE_CHARACTER = '\\'; + + /** + * The default strict quote behavior to use if none is supplied to the + * constructor + */ + public static final boolean DEFAULT_STRICT_QUOTES = false; + + /** + * The default leading whitespace behavior to use if none is supplied to the + * constructor + */ + public static final boolean DEFAULT_IGNORE_LEADING_WHITESPACE = true; + + /** + * I.E. if the quote character is set to null then there is no quote character. + */ + public static final boolean DEFAULT_IGNORE_QUOTATIONS = false; + + /** + * This is the "null" character - if a value is set to this then it is ignored. + */ + static final char NULL_CHARACTER = '\0'; + + /** + * Constructs CSVParser using a comma for the separator. + */ + public SimpleCsvParser() { + this(DEFAULT_SEPARATOR, DEFAULT_QUOTE_CHARACTER, DEFAULT_ESCAPE_CHARACTER); + } + + /** + * Constructs CSVParser with supplied separator. + * + * @param separator the delimiter to use for separating entries. + */ + public SimpleCsvParser(char separator) { + this(separator, DEFAULT_QUOTE_CHARACTER, DEFAULT_ESCAPE_CHARACTER); + } + + /** + * Constructs CSVParser with supplied separator and quote char. + * + * @param separator the delimiter to use for separating entries + * @param quotechar the character to use for quoted elements + */ + public SimpleCsvParser(char separator, char quotechar) { + this(separator, quotechar, DEFAULT_ESCAPE_CHARACTER); + } + + /** + * Constructs CSVReader with supplied separator and quote char. + * + * @param separator the delimiter to use for separating entries + * @param quotechar the character to use for quoted elements + * @param escape the character to use for escaping a separator or quote + */ + public SimpleCsvParser(char separator, char quotechar, char escape) { + this(separator, quotechar, escape, DEFAULT_STRICT_QUOTES); + } + + /** + * Constructs CSVParser with supplied separator and quote char. Allows setting + * the "strict quotes" flag + * + * @param separator the delimiter to use for separating entries + * @param quotechar the character to use for quoted elements + * @param escape the character to use for escaping a separator or quote + * @param strictQuotes if true, characters outside the quotes are ignored + */ + public SimpleCsvParser(char separator, char quotechar, char escape, boolean strictQuotes) { + this(separator, quotechar, escape, strictQuotes, DEFAULT_IGNORE_LEADING_WHITESPACE); + } + + /** + * Constructs CSVParser with supplied separator and quote char. Allows setting + * the "strict quotes" and "ignore leading whitespace" flags + * + * @param separator the delimiter to use for separating entries + * @param quotechar the character to use for quoted elements + * @param escapeCSVException the character to use for escaping a separator + * or quote + * @param strictQuotes if true, characters outside the quotes are + * ignored + * @param ignoreLeadingWhiteSpace if true, white space in front of a quote in a + * field is ignored + */ + public SimpleCsvParser(char separator, char quotechar, char escape, boolean strictQuotes, + boolean ignoreLeadingWhiteSpace) { + this(separator, quotechar, escape, strictQuotes, ignoreLeadingWhiteSpace, DEFAULT_IGNORE_QUOTATIONS); + } + + /** + * Constructs CSVParser with supplied separator and quote char. Allows setting + * the "strict quotes" and "ignore leading whitespace" flags + * + * @param separator the delimiter to use for separating entries + * @param quotechar the character to use for quoted elements + * @param escape the character to use for escaping a separator + * or quote + * @param strictQuotes if true, characters outside the quotes are + * ignored + * @param ignoreLeadingWhiteSpace if true, white space in front of a quote in a + * field is ignored + */ + public SimpleCsvParser(char separator, char quotechar, char escape, boolean strictQuotes, + boolean ignoreLeadingWhiteSpace, boolean ignoreQuotations) { + if (anyCharactersAreTheSame(separator, quotechar, escape)) { + throw new UnsupportedOperationException("The separator, quote, and escape characters must be different!"); + } + if (separator == NULL_CHARACTER) { + throw new UnsupportedOperationException("The separator character must be defined!"); + } + this.separator = separator; + this.quotechar = quotechar; + this.escape = escape; + this.strictQuotes = strictQuotes; + this.ignoreLeadingWhiteSpace = ignoreLeadingWhiteSpace; + this.ignoreQuotations = ignoreQuotations; + } + + private static boolean anyCharactersAreTheSame(char separator, char quotechar, char escape) { + return isSameCharacter(separator, quotechar) || isSameCharacter(separator, escape) + || isSameCharacter(quotechar, escape); + } + + private static boolean isSameCharacter(char c1, char c2) { + return c1 != NULL_CHARACTER && c1 == c2; + } + + /** + * @return true if something was left over from last call(s) + */ + public boolean isPending() { + return pending != null; + } + + /** + * Parses an incoming String and returns an array of elements. + * + * @param nextLine the string to parse + * @param multi multiline + * @return the comma-tokenized list of elements, or null if nextLine is null + * @throws CSVException if bad things happen during the read + */ + public String[] parseLine(String nextLine) throws CsvException { + + if (pending != null) { + pending = null; + } + + if (nextLine == null) { + if (pending != null) { + String s = pending; + pending = null; + return new String[] { s }; + } + return new String[0]; + } + + List tokensOnThisLine = new ArrayList<>(); + StringBuilder sb = new StringBuilder(INITIAL_READ_SIZE); + boolean inQuotes = false; + if (pending != null) { + sb.append(pending); + pending = null; + inQuotes = !this.ignoreQuotations; + } + int i = 0; + while (i < nextLine.length()) { + + char c = nextLine.charAt(i); + if (c == this.escape) { + if (isNextCharacterEscapable(nextLine, (inQuotes && !ignoreQuotations) || inField, i)) { + sb.append(nextLine.charAt(i + 1)); + i++; + } + } else if (c == quotechar) { + if (isNextCharacterEscapedQuote(nextLine, (inQuotes && !ignoreQuotations) || inField, i)) { + sb.append(nextLine.charAt(i + 1)); + i++; + } else { + inQuotes = !inQuotes; + + // the tricky case of an embedded quote in the middle: + // a,bc"d"ef,g + if (!strictQuotes && i > 2 // not on the beginning of the line + && nextLine.charAt(i - 1) != this.separator // not + // at + // the + // beginning + // of + // an + // escape + // sequence + && nextLine.length() > (i + 1) && nextLine.charAt(i + 1) != this.separator // not + // at + // the + // end + // of + // an + // escape + // sequence + ) { + + if (ignoreLeadingWhiteSpace && sb.length() > 0 && isAllWhiteSpace(sb)) { + sb = new StringBuilder(INITIAL_READ_SIZE); // discard + // white + // space + // leading + // up + // to + // quote + } else { + sb.append(c); + } + + } + } + inField = !inField; + } else if (c == separator && !(inQuotes && !ignoreQuotations)) { + tokensOnThisLine.add(sb.toString()); + sb = new StringBuilder(INITIAL_READ_SIZE); // start work on next + // token + inField = false; + } else { + if (!strictQuotes || (inQuotes && !ignoreQuotations)) { + sb.append(c); + inField = true; + } + } + i++; + } + // line is done - check status + if (inQuotes && !ignoreQuotations) { + throw new CsvException("Un-terminated quoted field at end of CSV line"); + } + if (sb != null) { + tokensOnThisLine.add(sb.toString()); + } + return tokensOnThisLine.toArray(new String[0]); + + } + + /** + * precondition: the current character is a quote or an escape + * + * @param nextLine the current line + * @param inQuotes true if the current context is quoted + * @param i current index in line + * @return true if the following character is a quote + */ + private boolean isNextCharacterEscapedQuote(String nextLine, boolean inQuotes, int i) { + return inQuotes // we are in quotes, therefore there can be escaped + // quotes in here. + && nextLine.length() > (i + 1) // there is indeed another + // character to check. + && nextLine.charAt(i + 1) == quotechar; + } + + /** + * precondition: the current character is an escape + * + * @param nextLine the current line + * @param inQuotes true if the current context is quoted + * @param i current index in line + * @return true if the following character is a quote + */ + protected boolean isNextCharacterEscapable(String nextLine, boolean inQuotes, int i) { + return inQuotes // we are in quotes, therefore there can be escaped + // quotes in here. + && nextLine.length() > (i + 1) // there is indeed another + // character to check. + && (nextLine.charAt(i + 1) == quotechar || nextLine.charAt(i + 1) == this.escape); + } + + /** + * precondition: sb.length() > 0 + * + * @param sb A sequence of characters to examine + * @return true if every character in the sequence is whitespace + */ + protected boolean isAllWhiteSpace(CharSequence sb) { + boolean result = true; + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + + if (!Character.isWhitespace(c)) { + return false; + } + } + return result; + } + + public Stream parse(URL url, Charset charset) { + try { + return parse(url.openStream(), charset); + } catch (IOException e) { + throw new CsvException(e); + } + } + + public Stream parse(InputStream in, Charset charset) { + BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset)); + return reader.lines().map(line -> parseLine(line)).onClose(() -> closeQuitely(reader)); + } + + @SuppressWarnings("PMD.EmptyCatchBlock") + private static void closeQuitely(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception e) { + // suppressed + } + } + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/BelfiorePart.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/BelfiorePart.java index b118b98..2fcf256 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/internal/BelfiorePart.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/BelfiorePart.java @@ -1,14 +1,11 @@ package it.kamaladafrica.codicefiscale.internal; -import static org.apache.commons.lang3.Validate.matchesPattern; - -import org.apache.commons.lang3.Validate; - -import com.google.common.primitives.ImmutableIntArray; +import java.util.Objects; import it.kamaladafrica.codicefiscale.City; import it.kamaladafrica.codicefiscale.city.CityByBelfiore; import it.kamaladafrica.codicefiscale.city.CityProvider; +import it.kamaladafrica.codicefiscale.utils.Validate; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -38,11 +35,11 @@ public static BelfiorePart from(String value) { public static BelfiorePart from(String value, CityByBelfiore provider) { Validate.notEmpty(value); - Validate.notNull(provider); - matchesPattern(value, VALIDATION_PATTERN); + Objects.requireNonNull(provider); + Validate.matchesPattern(value, VALIDATION_PATTERN); Omocode omocodeLevel = Omocode.of(value, OMOCODE_INDEXES); City input = toInput(omocodeLevel.normalize(value), provider); - Validate.notNull(input, "belfiore not found"); + Objects.requireNonNull(input, "belfiore not found"); return new BelfiorePart(input, omocodeLevel); } @@ -53,7 +50,7 @@ public static BelfiorePart of(City city) { @Override protected String computeValue() { String value = city.getBelfiore(); - matchesPattern(value, VALIDATION_PATTERN); + Validate.matchesPattern(value, VALIDATION_PATTERN); return value; } @@ -72,7 +69,7 @@ public BelfiorePart toOmocodeLevel(int level) { @Override protected void validateValue(String value) { - matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); + Validate.matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); } } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/ControlPart.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/ControlPart.java index a274d61..e437f7a 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/internal/ControlPart.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/ControlPart.java @@ -1,8 +1,7 @@ package it.kamaladafrica.codicefiscale.internal; -import static org.apache.commons.lang3.Validate.matchesPattern; - import it.kamaladafrica.codicefiscale.CodiceFiscale; +import it.kamaladafrica.codicefiscale.utils.Validate; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -27,7 +26,7 @@ public class ControlPart extends AbstractPart { String code; public static ControlPart of(String value) { - matchesPattern(value, VALIDATION_INPUT_PATTERN, "invalid value: %s", value); + Validate.matchesPattern(value, VALIDATION_INPUT_PATTERN, "invalid value: %s", value); return new ControlPart(value); } @@ -48,7 +47,7 @@ protected String computeValue() { int controlCharIndex = (even + odd) % 26; String value = String.valueOf((char) (CHAR_A + controlCharIndex)); - matchesPattern(value, VALIDATION_RESULT_PATTERN, "unexpected result: %s", value); + Validate.matchesPattern(value, VALIDATION_RESULT_PATTERN, "unexpected result: %s", value); return value; } @@ -71,13 +70,13 @@ public boolean isEqual(char controlChar) { } public boolean isEqual(String controlChar) { - matchesPattern(controlChar, VALIDATION_RESULT_PATTERN, "unexpected input: %s", controlChar); + Validate.matchesPattern(controlChar, VALIDATION_RESULT_PATTERN, "unexpected input: %s", controlChar); return getValue().equals(controlChar); } @Override protected void validateValue(String value) { - matchesPattern(value, VALIDATION_RESULT_PATTERN, "unexpected result: %s", value); + Validate.matchesPattern(value, VALIDATION_RESULT_PATTERN, "unexpected result: %s", value); } } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/DatePart.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/DatePart.java index dc2400b..8777b0a 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/internal/DatePart.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/DatePart.java @@ -1,13 +1,9 @@ package it.kamaladafrica.codicefiscale.internal; -import static org.apache.commons.lang3.Validate.matchesPattern; - import java.time.LocalDate; +import java.util.Objects; -import org.apache.commons.lang3.Validate; - -import com.google.common.primitives.ImmutableIntArray; - +import it.kamaladafrica.codicefiscale.utils.Validate; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -40,14 +36,14 @@ private DatePart(LocalDate date, boolean female, Omocode level) { public static DatePart from(String value) { Validate.notEmpty(value); - matchesPattern(value, VALIDATION_PATTERN); + Validate.matchesPattern(value, VALIDATION_PATTERN); final Omocode omocodeLevel = Omocode.of(value, OMOCODE_INDEXES); DatePartInput input = toInput(omocodeLevel.normalize(value)); return new DatePart(input.getDate(), input.isFemale(), omocodeLevel); } public static DatePart of(LocalDate date, boolean isFemale) { - Validate.notNull(date); + Objects.requireNonNull(date); return new DatePart(date, isFemale, Omocode.of(OMOCODE_INDEXES)); } @@ -62,7 +58,7 @@ protected String computeValue() { } String value = String.format(DATE_PART_FORMAT, year, MONTHS_CHARS.charAt(month), day); - matchesPattern(value, VALIDATION_PATTERN); + Validate.matchesPattern(value, VALIDATION_PATTERN); return value; } @@ -95,7 +91,7 @@ public DatePart toOmocodeLevel(int level) { @Override protected void validateValue(String value) { - matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); + Validate.matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); } @Value diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/ImmutableIntArray.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/ImmutableIntArray.java new file mode 100644 index 0000000..1c42bf2 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/ImmutableIntArray.java @@ -0,0 +1,50 @@ +package it.kamaladafrica.codicefiscale.internal; + +import it.kamaladafrica.codicefiscale.utils.ArrayUtils; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.Value; + +@Value(staticConstructor = "wrap") +public class ImmutableIntArray { + + @NonNull + @Getter(AccessLevel.NONE) + private final int[] array; + + public int get(int index) { + return array[index]; + } + + public int indexOf(int value) { + for (int i = 0; i < array.length; i++) { + if (get(i) == value) { + return i; + } + } + return -1; + } + + public boolean contains(int value) { + return indexOf(value) >= 0; + } + + public static ImmutableIntArray of(int... array) { + return wrap(ArrayUtils.copyOf(array)); + } + + public int length() { + return array.length; + } + + public int[] toArray() { + return ArrayUtils.copyOf(array); + } + + public ImmutableIntArray reverse() { + int[] array = toArray(); + ArrayUtils.reverse(array); + return wrap(array); + } +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/LastnamePart.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/LastnamePart.java index f4999cd..e36c8a8 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/internal/LastnamePart.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/LastnamePart.java @@ -3,11 +3,9 @@ import static it.kamaladafrica.codicefiscale.utils.PartUtils.extractConsonants; import static it.kamaladafrica.codicefiscale.utils.PartUtils.extractVowels; import static it.kamaladafrica.codicefiscale.utils.PartUtils.normalizeString; -import static org.apache.commons.lang3.Validate.matchesPattern; - -import org.apache.commons.lang3.Validate; import it.kamaladafrica.codicefiscale.utils.PartUtils; +import it.kamaladafrica.codicefiscale.utils.Validate; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -28,7 +26,7 @@ public class LastnamePart extends AbstractPart { public static LastnamePart from(String value) { Validate.notEmpty(value); - matchesPattern(value, VALIDATION_PATTERN, "invalid value: %s", value); + Validate.matchesPattern(value, VALIDATION_PATTERN, "invalid value: %s", value); value = PartUtils.removePlaceholderIfPresent(value); return of(value); } @@ -48,7 +46,7 @@ protected String computeValue() { part.append(MISSING_LETTERS_PLACEHOLDER); part.setLength(PART_LENGTH); - matchesPattern(part, VALIDATION_PATTERN, "unexpected result: %s", part); + Validate.matchesPattern(part, VALIDATION_PATTERN, "unexpected result: %s", part); return part.toString(); } @@ -60,7 +58,7 @@ protected String applyOmocodeLevel(String computeValue) { @Override protected void validateValue(String value) { - matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); + Validate.matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); } } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/NamePart.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/NamePart.java index a95cb66..2755ebb 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/internal/NamePart.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/NamePart.java @@ -3,11 +3,9 @@ import static it.kamaladafrica.codicefiscale.utils.PartUtils.extractConsonants; import static it.kamaladafrica.codicefiscale.utils.PartUtils.extractVowels; import static it.kamaladafrica.codicefiscale.utils.PartUtils.normalizeString; -import static org.apache.commons.lang3.Validate.matchesPattern; - -import org.apache.commons.lang3.Validate; import it.kamaladafrica.codicefiscale.utils.PartUtils; +import it.kamaladafrica.codicefiscale.utils.Validate; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -30,7 +28,7 @@ public class NamePart extends AbstractPart { public static NamePart from(String value) { Validate.notEmpty(value); - matchesPattern(value, VALIDATION_PATTERN, "invalid value: %s", value); + Validate.matchesPattern(value, VALIDATION_PATTERN, "invalid value: %s", value); value = PartUtils.removePlaceholderIfPresent(value); return of(value); } @@ -50,17 +48,15 @@ protected String computeValue() { part.append(MISSING_LETTERS_PLACEHOLDER); part.setLength(PART_LENGTH); - matchesPattern(part, VALIDATION_PATTERN, "unexpected result: %s", part); + Validate.matchesPattern(part, VALIDATION_PATTERN, "unexpected result: %s", part); return part.toString(); } private String nameConsonants(String name) { String consonants = extractConsonants(name); - if(consonants.length() > PART_LENGTH){ - consonants = new StringBuilder(consonants) - .deleteCharAt(INDEX_TO_REMOVE) - .toString(); + if (consonants.length() > PART_LENGTH) { + consonants = new StringBuilder(consonants).deleteCharAt(INDEX_TO_REMOVE).toString(); } return consonants; } @@ -72,7 +68,7 @@ protected String applyOmocodeLevel(String computeValue) { @Override protected void validateValue(String value) { - matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); + Validate.matchesPattern(value, VALIDATION_PATTERN, "unexpected result: %s", value); } } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/internal/Omocode.java b/src/main/java/it/kamaladafrica/codicefiscale/internal/Omocode.java index c79cca6..58e28fd 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/internal/Omocode.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/internal/Omocode.java @@ -1,11 +1,6 @@ package it.kamaladafrica.codicefiscale.internal; -import static org.apache.commons.lang3.Validate.inclusiveBetween; - -import com.google.common.collect.Lists; -import com.google.common.primitives.ImmutableIntArray; -import com.google.common.primitives.Ints; - +import it.kamaladafrica.codicefiscale.utils.Validate; import lombok.AccessLevel; import lombok.Getter; import lombok.Value; @@ -24,7 +19,7 @@ public class Omocode { ImmutableIntArray omocodeIndices; private Omocode(int level, int mask, ImmutableIntArray omocodeIndices) { - inclusiveBetween(0, mask, level, "invalid omocode level: 0 <= %s <= %s", level, mask); + Validate.inclusiveBetween(0, mask, level, "invalid omocode level: 0 <= %s <= %s", level, mask); this.level = level; this.mask = mask; this.omocodeIndices = omocodeIndices; @@ -85,7 +80,7 @@ public Omocode ofValue(String value) { public static Omocode of(int level, int[] omocodeIndices) { int mask = ~(~0 << omocodeIndices.length); // 2^omocodeIndices.length - 1 - ImmutableIntArray indices = ImmutableIntArray.copyOf(Lists.reverse(Ints.asList(omocodeIndices))); + ImmutableIntArray indices = ImmutableIntArray.wrap(omocodeIndices).reverse(); return new Omocode(level, mask, indices); } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/utils/ArrayUtils.java b/src/main/java/it/kamaladafrica/codicefiscale/utils/ArrayUtils.java new file mode 100644 index 0000000..a36c41b --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/utils/ArrayUtils.java @@ -0,0 +1,26 @@ +package it.kamaladafrica.codicefiscale.utils; + +import java.util.Arrays; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ArrayUtils { + + public static void reverse(int[] array) { + int i = 0; + int j = array.length - 1; + while (i < j) { + int x = array[i]; + array[i] = array[j]; + array[j] = x; + i++; + j--; + } + } + + public static int[] copyOf(int[] array) { + return Arrays.copyOf(array, array.length); + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/utils/Pair.java b/src/main/java/it/kamaladafrica/codicefiscale/utils/Pair.java new file mode 100644 index 0000000..d4d26f0 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/utils/Pair.java @@ -0,0 +1,31 @@ +package it.kamaladafrica.codicefiscale.utils; + +import java.io.Serializable; +import java.util.Map; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Pair implements Map.Entry, Serializable { + + private static final long serialVersionUID = -4440235638868672592L; + + private final L left; + private final R right; + + @Override + public L getKey() { + return left; + } + + @Override + public R getValue() { + return right; + } + + @Override + public R setValue(R value) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/utils/PartUtils.java b/src/main/java/it/kamaladafrica/codicefiscale/utils/PartUtils.java index 3845135..bc97c52 100644 --- a/src/main/java/it/kamaladafrica/codicefiscale/utils/PartUtils.java +++ b/src/main/java/it/kamaladafrica/codicefiscale/utils/PartUtils.java @@ -1,9 +1,6 @@ package it.kamaladafrica.codicefiscale.utils; -import static org.apache.commons.lang3.StringUtils.defaultString; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; +import java.util.Objects; import it.kamaladafrica.codicefiscale.CodiceFiscale; import lombok.experimental.UtilityClass; @@ -40,17 +37,17 @@ public class PartUtils { public static String normalizeString(String s) { return RegexUtils.extract(RegexUtils.CF_ALLOWED_CHARS, - StringUtils.replaceEach(defaultString(s).toUpperCase(CodiceFiscale.LOCALE),DIACRITICS, DIACRITICS_TRANSLITERATION)); + StringUtils.replaceEach(StringUtils.defaultString(s).toUpperCase(CodiceFiscale.LOCALE),DIACRITICS, DIACRITICS_TRANSLITERATION)); } public static String extractConsonants(String s) { - Validate.notNull(s); + Objects.requireNonNull(s); return RegexUtils.extract(RegexUtils.CONSONANT_PATTERN, s); } public static String extractVowels(String s) { - Validate.notNull(s); + Objects.requireNonNull(s); return RegexUtils.extract(RegexUtils.VOWEL_PATTERN, s); } diff --git a/src/main/java/it/kamaladafrica/codicefiscale/utils/StringUtils.java b/src/main/java/it/kamaladafrica/codicefiscale/utils/StringUtils.java new file mode 100644 index 0000000..5026e52 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/utils/StringUtils.java @@ -0,0 +1,69 @@ +package it.kamaladafrica.codicefiscale.utils; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class StringUtils { + + public static final String EMPTY_STRING = ""; + + public static String defaultString(String s, String defaultString) { + return s == null ? defaultString : s; + } + + public static String defaultString(String s) { + return defaultString(s, EMPTY_STRING); + } + + public static String removeEnd(String s, String remove) { + if (s == null) { + return null; + } + if (remove == null) { + return s; + } + if (s.endsWith(remove)) { + return s.substring(0, s.lastIndexOf(remove)); + } + return s; + } + + public static String replaceEach(String s, String[] searchList, String[] replaceList) { + if (s == null) { + return null; + } + if (searchList == null || replaceList == null) { + return s; + } + if (searchList.length != replaceList.length) { + throw new IllegalArgumentException("searchList and replaceList must have same size"); + } + + for (int i = 0; i < replaceList.length; i++) { + if (searchList[i] != null && replaceList[i] != null) { + s = s.replaceAll(searchList[i], replaceList[i]); + } + } + return s; + } + + public static boolean equals(final CharSequence cs1, final CharSequence cs2) { + if (cs1 == cs2) { + return true; + } + if (cs1 == null || cs2 == null || (cs1.length() != cs2.length())) { + return false; + } + if (cs1 instanceof String && cs2 instanceof String) { + return cs1.equals(cs2); + } + // Step-wise comparison + final int length = cs1.length(); + for (int i = 0; i < length; i++) { + if (cs1.charAt(i) != cs2.charAt(i)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/it/kamaladafrica/codicefiscale/utils/Validate.java b/src/main/java/it/kamaladafrica/codicefiscale/utils/Validate.java new file mode 100644 index 0000000..7bc97a6 --- /dev/null +++ b/src/main/java/it/kamaladafrica/codicefiscale/utils/Validate.java @@ -0,0 +1,54 @@ +package it.kamaladafrica.codicefiscale.utils; + +import java.util.Objects; +import java.util.regex.Pattern; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Validate { + + public T notEmpty(T s, String message, Object... args) { + if (Objects.requireNonNull(s).length() == 0) { + throw new IllegalArgumentException(String.format(message, args)); + } + return s; + } + + public T notEmpty(T s) { + return notEmpty(s, "The string is empty"); + } + + public T matchesPattern(T s, String pattern, String message, Object... args) { + if (!Pattern.matches(pattern, Objects.requireNonNull(s))) { + throw new IllegalArgumentException(String.format(message, args)); + } + return s; + } + + public T matchesPattern(T s, String pattern) { + return matchesPattern(s, pattern, "The string %s does not match the pattern %s", s, pattern); + } + + public int inclusiveBetween(int lowerBound, int upperBound, int value, String message, Object... args) { + if (value < lowerBound || value > upperBound) { + throw new IllegalArgumentException(String.format(message, args)); + } + return value; + } + + public int inclusiveBetween(int lowerBound, int upperBound, int value) { + return inclusiveBetween(lowerBound, upperBound, value, "%s is not %s <= %s <= %s", value, lowerBound, value, + upperBound); + } + + public T validIndex(T value, int index, String message, Object... args) { + inclusiveBetween(0, value.length() - 1, index, message, args); + return value; + } + + public T validIndex(T value, int index) { + return validIndex(value, index, "invalid index 0 <= %s <= %s", index, value.length()); + } + +} diff --git a/src/main/resources/esteri-cessati.csv b/src/main/resources/it/kamaladafrica/codicefiscale/city/impl/esteri-cessati.csv similarity index 100% rename from src/main/resources/esteri-cessati.csv rename to src/main/resources/it/kamaladafrica/codicefiscale/city/impl/esteri-cessati.csv diff --git a/src/main/resources/esteri.csv b/src/main/resources/it/kamaladafrica/codicefiscale/city/impl/esteri.csv similarity index 100% rename from src/main/resources/esteri.csv rename to src/main/resources/it/kamaladafrica/codicefiscale/city/impl/esteri.csv diff --git a/src/main/resources/italia.csv b/src/main/resources/it/kamaladafrica/codicefiscale/city/impl/italia.csv similarity index 100% rename from src/main/resources/italia.csv rename to src/main/resources/it/kamaladafrica/codicefiscale/city/impl/italia.csv diff --git a/src/test/java/it/kamaladafrica/codicefiscale/CodiceFiscaleTest.java b/src/test/java/it/kamaladafrica/codicefiscale/CodiceFiscaleTest.java index c6bf8d0..863ea01 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/CodiceFiscaleTest.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/CodiceFiscaleTest.java @@ -65,7 +65,10 @@ public void testToOmocodeLevel() { assertTrue(CodiceFiscale.of(CODICE_FISCALE_1).toOmocodeLevel(0).isEqual(CODICE_FISCALE, false)); assertTrue(CodiceFiscale.of(CODICE_FISCALE_1).toOmocodeLevel(3).isEqual(CODICE_FISCALE_2, false)); assertFalse(CodiceFiscale.of(CODICE_FISCALE_1).toOmocodeLevel(3).isEqual(CODICE_FISCALE, false)); - } + + assertThrows(IllegalArgumentException.class, () -> CodiceFiscale.of(CODICE_FISCALE_1).toOmocodeLevel(-5)); + assertThrows(IllegalArgumentException.class, () -> CodiceFiscale.of(CODICE_FISCALE_1).toOmocodeLevel(200)); +} @Test public void testOfString() { @@ -113,6 +116,7 @@ public void testIsFormatValid() { @Test public void testValidate() { assertThrows(IllegalArgumentException.class, () -> CodiceFiscale.validate("RSSMRA75C21H501X")); + assertThrows(IllegalArgumentException.class, () -> CodiceFiscale.validate("RSSMRAXXC21H501X")); } @Test diff --git a/src/test/java/it/kamaladafrica/codicefiscale/Issue30Test.java b/src/test/java/it/kamaladafrica/codicefiscale/Issue30Test.java index 3fa6250..340cc68 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/Issue30Test.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/Issue30Test.java @@ -1,9 +1,10 @@ package it.kamaladafrica.codicefiscale; -import it.kamaladafrica.codicefiscale.city.CityProvider; +import static org.junit.Assert.assertEquals; + import org.junit.Test; -import static org.junit.Assert.assertEquals; +import it.kamaladafrica.codicefiscale.city.CityProvider; public class Issue30Test { diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritmTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritmTest.java new file mode 100644 index 0000000..35559c3 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritmTest.java @@ -0,0 +1,26 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class ExactMatchAlgoritmTest { + + ExactMatchAlgoritm uut = new ExactMatchAlgoritm(); + + @Test + public void testGetExactMatchAlgoritm_NullNull() { + assertThrows(IllegalArgumentException.class, () -> uut.apply(null, null)); + } + + @Test + public void testGetExactMatchAlgoritm_NullString() { + assertThrows(IllegalArgumentException.class, () -> uut.apply(null, "clear")); + } + + @Test + public void testExactMatchAlgoritm_StringNull() { + assertThrows(IllegalArgumentException.class, () -> uut.apply(" ", null)); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritm_applyTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritm_applyTest.java new file mode 100644 index 0000000..b0d27e6 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/ExactMatchAlgoritm_applyTest.java @@ -0,0 +1,56 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +import static it.kamaladafrica.codicefiscale.utils.TestUtils.wrap; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ExactMatchAlgoritm_applyTest { + + @Parameter(0) + public boolean ignoreCase; + + @Parameter(1) + public CharSequence left; + + @Parameter(2) + public CharSequence right; + + @Parameter(3) + public double expected; + + ExactMatchAlgoritm uutIgnoreCase = new ExactMatchAlgoritm(true); + ExactMatchAlgoritm uutCaseSensitive = new ExactMatchAlgoritm(false); + + @Test + public void testGetExactMatchAlgoritm_StringString() { + ExactMatchAlgoritm uut = ignoreCase ? uutIgnoreCase : uutCaseSensitive; + assertEquals(expected, uut.apply(left, right), 0.0); + } + + @Parameters(name = "[{0}] apply(\"{1}\", \"{2}\")") + public static Iterable data() { + return Arrays.asList(new Object[][] { + { false, wrap(""), "", 1.0 }, + { true, wrap(""), "", 1.0 }, + { false, wrap("aaa"), "aaa", 1.0 }, + { false, wrap("aaa"), "AAA", 0.0 }, + { true, wrap("aaa"), "AAA", 1.0 }, + { true, wrap("aAa"), "AaA", 1.0 }, + { false, wrap("aaa"), "bbb", 0.0 }, + { true, wrap("AAA"), "BBB", 0.0 }, + { true, wrap("AAA"), "AAAA", 0.0 }, + { true, "AAA", "AAA", 1.0 }, + { true, new String("AAA"), new StringBuilder("AAA"), 1.0 }, + { false, new String("aaa"), new String("aaa"), 1.0 }, + { true, new String("aaa"), new String("AAA"), 1.0 }, + }); + } +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritmTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritmTest.java new file mode 100644 index 0000000..6ff8c41 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritmTest.java @@ -0,0 +1,26 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class JaroWinklerAlgoritmTest { + + JaroWinklerAlgoritm uut = new JaroWinklerAlgoritm(); + + @Test + public void testGetJaroWinklerSimilarity_NullNull() { + assertThrows(IllegalArgumentException.class, () -> uut.apply(null, null)); + } + + @Test + public void testGetJaroWinklerSimilarity_NullString() { + assertThrows(IllegalArgumentException.class, () -> uut.apply(null, "clear")); + } + + @Test + public void testGetJaroWinklerSimilarity_StringNull() { + assertThrows(IllegalArgumentException.class, () -> uut.apply(" ", null)); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritm_applyTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritm_applyTest.java new file mode 100644 index 0000000..32fe89c --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/algo/JaroWinklerAlgoritm_applyTest.java @@ -0,0 +1,58 @@ +package it.kamaladafrica.codicefiscale.city.algo; + +import static it.kamaladafrica.codicefiscale.utils.TestUtils.wrap; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class JaroWinklerAlgoritm_applyTest { + + JaroWinklerAlgoritm uut = new JaroWinklerAlgoritm(); + + @Parameter(0) + public String left; + + @Parameter(1) + public String right; + + @Parameter(2) + public double expected; + + @Parameter(3) + public double tolerance; + + @Before + public void setUp() { + uut = new JaroWinklerAlgoritm(); + } + + @Test + public void testGetJaroWinklerSimilarity_StringString() { + assertEquals(expected, uut.apply(wrap(left), right), tolerance); + } + + @Parameters(name = "apply(\"{0}\", \"{1}\")") + public static Iterable data() { + return Arrays.asList(new Object[][] { { "", "", 1.0, 0.00001d }, { "foo", "foo", 1.0, 0.00001d }, + { "foo", "foo ", 0.94166d, 0.00001d }, { "foo", "foo ", 0.90666d, 0.00001d }, + { "foo", " foo ", 0.86666d, 0.00001d }, { "foo", " foo", 0.51111d, 0.00001d }, + { "fog", "frog", 0.92499d, 0.00001d }, { "fly", "ant", 0.0d, 0.00000000000000000001d }, + { "elephant", "hippo", 0.44166d, 0.00001d }, { "ABC Corporation", "ABC Corp", 0.90666d, 0.00001d }, + { "D N H Enterprises Inc", "D & H Enterprises, Inc.", 0.95251d, 0.00001d }, + { "My Gym Children's Fitness Center", "My Gym. Childrens Fitness", 0.942d, 0.00001d }, + { "PENNSYLVANIA", "PENNCISYLVNIA", 0.898018d, 0.00001d }, + { "/opt/software1", "/opt/software2", 0.971428d, 0.00001d }, + { "aaabcd", "aaacdb", 0.941666d, 0.00001d }, { "John Horn", "John Hopkins", 0.911111d, 0.00001d }, + + }); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/AbstractCsvStreamSupplierTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/AbstractCsvStreamSupplierTest.java new file mode 100644 index 0000000..a278b05 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/AbstractCsvStreamSupplierTest.java @@ -0,0 +1,54 @@ +package it.kamaladafrica.codicefiscale.city.impl; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Test; + +import it.kamaladafrica.codicefiscale.city.impl.csv.AbstractCsvStreamSupplier; +import it.kamaladafrica.codicefiscale.city.impl.csv.CsvException; + +public class AbstractCsvStreamSupplierTest { + + @Test + public void testGetException() throws MalformedURLException { + AbstractCsvStreamSupplier supplier = new AbstractCsvStreamSupplier( + new URL("file:///not_existing_file"), ';') { + @Override + protected String mapper(String[] row) { + return row.toString(); + } + }; + assertThrows(CsvException.class, () -> supplier.get()); + } + + @Test + public void testGet() throws MalformedURLException { + AbstractCsvStreamSupplier supplier = new AbstractCsvStreamSupplier( + getClass().getResource(CityProviderImpl.ITALIA_RESOURCE_PATH), ';') { + @Override + protected String mapper(String[] row) { + return row.toString(); + } + }; + try { + supplier.get(); + } catch (Exception e) { + fail(); + } + } + + @Test + public void testNullResource() throws MalformedURLException { + assertThrows(NullPointerException.class, () -> new AbstractCsvStreamSupplier(null, ';') { + @Override + protected String mapper(String[] row) { + return row.toString(); + } + }); + + } +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImplTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImplTest.java index 5a70b27..bef32c0 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImplTest.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CityProviderImplTest.java @@ -8,6 +8,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -15,22 +17,23 @@ import org.junit.Assume; import org.junit.Test; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; - import it.kamaladafrica.codicefiscale.City; +import it.kamaladafrica.codicefiscale.utils.TestUtils; public class CityProviderImplTest { - private final Map CITIES = ImmutableMap.builder() - .orderEntriesByValue((o1, o2) -> o2.getName().compareTo(o1.getName())) - .put("CAPENA(RM)", City.builder().name("CAPENA").prov("RM").belfiore("A123").build()) - .put("MORLUPO(RM)", City.builder().name("MORLUPO").prov("RM").belfiore("B456").build()) - .put("ZAGAROLO(RM)", City.builder().name("ZAGAROLO").prov("RM").belfiore("C789").build()).build(); + private final Map CITIES = Collections.unmodifiableMap(new LinkedHashMap() { + private static final long serialVersionUID = 1L; + { + put("ZAGAROLO(RM)", City.builder().name("ZAGAROLO").prov("RM").belfiore("C789").build()); + put("MORLUPO(RM)", City.builder().name("MORLUPO").prov("RM").belfiore("B456").build()); + put("CAPENA(RM)", City.builder().name("CAPENA").prov("RM").belfiore("A123").build()); + } + }); @Test public void testOfSet() { - final Set source = ImmutableSet.copyOf(CITIES.values()); + final Set source = TestUtils.unmodifiableSet(CITIES.values()); Assume.assumeThat(source.iterator().next(), is(CITIES.get("ZAGAROLO(RM)"))); final CityProviderImpl provider = CityProviderImpl.of(source); @@ -43,7 +46,7 @@ public void testOfSet() { @Test public void testOfSupplier() { - final Set source = ImmutableSet.copyOf(CITIES.values()); + final Set source = TestUtils.unmodifiableSet(CITIES.values()); Assume.assumeThat(source.iterator().next(), is(CITIES.get("ZAGAROLO(RM)"))); final CityProviderImpl provider = CityProviderImpl.of(() -> source); @@ -56,7 +59,7 @@ public void testOfSupplier() { @Test public void testOfSetScore() { - final Set source = ImmutableSet.copyOf(CITIES.values()); + final Set source = TestUtils.unmodifiableSet(CITIES.values()); Assume.assumeThat(source.iterator().next(), is(CITIES.get("ZAGAROLO(RM)"))); final CityProviderImpl provider = CityProviderImpl.of(source, CityProviderImpl.EXACT_MATCH_SCORE); @@ -73,7 +76,7 @@ public void testOfSetScore() { @Test public void testOfSupplierScore() { - final Set source = ImmutableSet.copyOf(CITIES.values()); + final Set source = TestUtils.unmodifiableSet(CITIES.values()); Assume.assumeThat(source.iterator().next(), is(CITIES.get("ZAGAROLO(RM)"))); final CityProviderImpl provider = CityProviderImpl.of(() -> source, CityProviderImpl.EXACT_MATCH_SCORE); @@ -107,7 +110,7 @@ public void testOfDefault() { @Test public void testFindAll() { - final CityProviderImpl provider = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl provider = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.EXACT_MATCH_SCORE); final List cities = provider.findAll(); assertEquals(CITIES.size(), cities.size()); @@ -116,14 +119,14 @@ public void testFindAll() { @Test public void testFindByNameExact() { - final CityProviderImpl providerExact = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl providerExact = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.EXACT_MATCH_SCORE); assertEquals("MORLUPO", providerExact.findByName("MORLUPO(RM)").getName()); } @Test public void testFindByNameNotExact() { - final CityProviderImpl providerNotExact = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl providerNotExact = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.DEFAULT_MINIMUM_MATCH_SCORE); assertEquals("MORLUPO", providerNotExact.findByName("MORLUPO(RM)").getName()); assertEquals("MORLUPO", providerNotExact.findByName("MORLUPO (RM)").getName()); @@ -135,36 +138,37 @@ public void testFindByNameNotExact() { @Test(expected = IllegalArgumentException.class) public void testFindByNameExactNotFound() { - final CityProviderImpl providerExact = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl providerExact = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.EXACT_MATCH_SCORE); providerExact.findByName("MORLOPE"); } @Test(expected = IllegalArgumentException.class) public void testFindByNameEmptyTerm() { - final CityProviderImpl provider = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl provider = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.EXACT_MATCH_SCORE); provider.findByName(null); } @Test(expected = IllegalArgumentException.class) public void testFindByNameNotExactNotFound() { - final CityProviderImpl providerExact = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl providerExact = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.DEFAULT_MINIMUM_MATCH_SCORE); providerExact.findByName("XXXX"); } @Test public void testFindByBelfiore() { - final CityProviderImpl provider = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl provider = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.EXACT_MATCH_SCORE); assertEquals("MORLUPO", provider.findByBelfiore("B456").getName()); } @Test public void testFindByBelfioreNotFound() { - final CityProviderImpl provider = CityProviderImpl.of(ImmutableSet.copyOf(CITIES.values()), + final CityProviderImpl provider = CityProviderImpl.of(TestUtils.unmodifiableSet(CITIES.values()), CityProviderImpl.EXACT_MATCH_SCORE); assertThrows(IllegalArgumentException.class, () -> provider.findByBelfiore("XXXX")); } + } diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CompositeCityStreamSupplierTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CompositeCityStreamSupplierTest.java new file mode 100644 index 0000000..334d972 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CompositeCityStreamSupplierTest.java @@ -0,0 +1,51 @@ +package it.kamaladafrica.codicefiscale.city.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Test; + +import it.kamaladafrica.codicefiscale.City; + +public class CompositeCityStreamSupplierTest { + + CompositeCityStreamSupplier uut = CompositeCityStreamSupplier.of( + () -> Stream.of( + City.builder().name("ROMA").prov("RM").belfiore("A").build(), + City.builder().name("VITERBO").prov("VT").belfiore("B").build()), + () -> Stream.of(City.builder().name("MILANO").prov("MI").belfiore("C").build()), + () -> Stream.of( + City.builder().name("GENOVA").prov("GE").belfiore("D").build(), + City.builder().name("LA SPEZIA").prov("SP").belfiore("E").build())); + + @Test + public void testGet() { + List result = uut.get().map(City::getName).collect(Collectors.toList()); + assertEquals(5, result.size()); + assertTrue(result.contains("ROMA")); + assertTrue(result.contains("VITERBO")); + assertTrue(result.contains("MILANO")); + assertTrue(result.contains("GENOVA")); + assertTrue(result.contains("LA SPEZIA")); + } + + @Test + public void testOfCityStreamSupplierCityStreamSupplierArray() { + assertEquals(3, uut.getSuppliers().length); + assertEquals(1, + CompositeCityStreamSupplier + .of(() -> Stream.of(City.builder().name("ROMA").prov("RM").belfiore("A").build()), + new CompositeCityStreamSupplier[0]) + .getSuppliers().length); + assertEquals(1, + CompositeCityStreamSupplier + .of(() -> Stream.of(City.builder().name("ROMA").prov("RM").belfiore("A").build()), + (CompositeCityStreamSupplier[]) null) + .getSuppliers().length); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CsvSupplierTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CsvSupplierTest.java deleted file mode 100644 index 2546f9f..0000000 --- a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/CsvSupplierTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package it.kamaladafrica.codicefiscale.city.impl; - -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.fail; - -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.csv.CSVFormat; -import org.junit.Test; - -public class CsvSupplierTest { - - @Test - public void testGetException() throws MalformedURLException { - CsvSupplier supplier = new CsvSupplier(new URL("file:///not_existing_file"), StandardCharsets.UTF_8, - CSVFormat.DEFAULT, e -> null); - assertThrows(IllegalArgumentException.class, () -> supplier.get()); - } - - @Test - public void testGet() throws MalformedURLException { - CsvSupplier supplier = new CsvSupplier(getClass().getResource(CityProviderImpl.ITALIA_RESOURCE_PATH), - StandardCharsets.UTF_8, CSVFormat.DEFAULT, e -> null); - try { - supplier.get(); - } catch (Exception e) { - fail(); - } - } -} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplierTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplierTest.java index 537c1d6..759f10b 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplierTest.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/EsteriCsvSupplierTest.java @@ -1,6 +1,6 @@ package it.kamaladafrica.codicefiscale.city.impl; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; import java.util.Set; import java.util.stream.Collectors; @@ -9,9 +9,10 @@ import org.junit.Test; import it.kamaladafrica.codicefiscale.City; +import it.kamaladafrica.codicefiscale.city.impl.csv.EsteriCsvSupplier; public class EsteriCsvSupplierTest { - + private static final String ENCODING_ERROR_MARKER = "\uFFFD"; @Test diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplierTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplierTest.java index e3365ec..51a37d5 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplierTest.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/ItaliaCsvSupplierTest.java @@ -9,6 +9,7 @@ import org.junit.Test; import it.kamaladafrica.codicefiscale.City; +import it.kamaladafrica.codicefiscale.city.impl.csv.ItaliaCsvSupplier; public class ItaliaCsvSupplierTest { diff --git a/src/test/java/it/kamaladafrica/codicefiscale/city/impl/csv/SimpleCsvParserTest.java b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/csv/SimpleCsvParserTest.java new file mode 100644 index 0000000..6e8f3e5 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/city/impl/csv/SimpleCsvParserTest.java @@ -0,0 +1,42 @@ +package it.kamaladafrica.codicefiscale.city.impl.csv; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; + +public class SimpleCsvParserTest { + + SimpleCsvParser uut = new SimpleCsvParser(';', '"'); + + @Test + public void testParseSingleLine() throws IOException { + String csv = "A;B;C\nPino;\"1\";2\nGino;\"3\";4\nFranco;\"5\";\"\\\"6\\\"\""; + ByteArrayInputStream in = new ByteArrayInputStream(csv.getBytes(StandardCharsets.UTF_8)); + List result = uut.parse(in, StandardCharsets.UTF_8).skip(1).collect(Collectors.toList()); + + List colA = result.stream().map(s -> s[0]).collect(Collectors.toList()); + List colB = result.stream().map(s -> s[1]).collect(Collectors.toList()); + List colC = result.stream().map(s -> s[2]).collect(Collectors.toList()); + + assertEquals(3, colA.size()); + assertEquals(3, colB.size()); + assertEquals(3, colC.size()); + + assertFalse(colA.contains("A")); + assertFalse(colB.contains("B")); + assertFalse(colC.contains("C")); + + assertEquals(1, colA.indexOf("Gino")); + assertEquals(0, colB.indexOf("1")); + assertEquals(2, colC.indexOf("\"6\"")); + + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/internal/ImmutableIntArrayTest.java b/src/test/java/it/kamaladafrica/codicefiscale/internal/ImmutableIntArrayTest.java new file mode 100644 index 0000000..5385480 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/internal/ImmutableIntArrayTest.java @@ -0,0 +1,84 @@ +package it.kamaladafrica.codicefiscale.internal; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ImmutableIntArrayTest { + + ImmutableIntArray uut = ImmutableIntArray.of(1, 2, 3); + + @Test + public void testGet() { + assertEquals(3, uut.get(2)); + assertEquals(1, uut.get(0)); + + assertThrows(ArrayIndexOutOfBoundsException.class, () -> uut.get(-1)); + assertThrows(ArrayIndexOutOfBoundsException.class, () -> uut.get(4)); + } + + @Test + public void testIndexOf() { + assertEquals(0, uut.indexOf(1)); + assertEquals(1, uut.indexOf(2)); + assertEquals(2, uut.indexOf(3)); + + assertEquals(-1, uut.indexOf(4)); + } + + @Test + public void testContains() { + assertTrue(uut.contains(1)); + assertTrue(uut.contains(2)); + assertTrue(uut.contains(3)); + + assertFalse(uut.contains(4)); + } + + @Test + public void testOf() { + int[] array = new int[] { 1, 2, 3 }; + ImmutableIntArray expected = ImmutableIntArray.of(array); + assertEquals(expected, uut); + + array[1] = 5; + assertFalse(expected.contains(5)); + + assertEquals(ImmutableIntArray.wrap(new int[0]), ImmutableIntArray.of()); + } + + @Test + public void testLength() { + assertEquals(3, uut.length()); + } + + @Test + public void testToArray() { + int[] array = new int[] { 1, 2, 3 }; + assertArrayEquals(array, uut.toArray()); + + ImmutableIntArray other = ImmutableIntArray.of(array); + array[0] = 5; + assertArrayEquals(new int[] { 1, 2, 3 }, other.toArray()); + + } + + @Test + public void testReverse() { + ImmutableIntArray reversed = uut.reverse(); + assertArrayEquals(new int[] {3,2,1}, reversed.toArray()); + } + + @Test + public void testWrap() { + int[] array = new int[] {1,2,3}; + ImmutableIntArray uut = ImmutableIntArray.wrap(array); + array[0] = 5; + assertTrue(uut.contains(5)); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/internal/OmocodeTest.java b/src/test/java/it/kamaladafrica/codicefiscale/internal/OmocodeTest.java index eed843c..7a886ee 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/internal/OmocodeTest.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/internal/OmocodeTest.java @@ -2,18 +2,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import org.junit.Test; -import com.google.common.primitives.ImmutableIntArray; - public class OmocodeTest { @Test public void test_of() { assertTrue(Omocode.of(new int[0]).isSameLevel(Omocode.of(ImmutableIntArray.of()))); + assertEquals(1, Omocode.of("AAAN", ImmutableIntArray.of(3)).getLevel()); } @Test @@ -22,6 +22,8 @@ public void test_apply() { for (int i = 0; i < 7; i++) { assertEquals(res[i], Omocode.of(i + 1, new int[] { 1, 2, 4 }).apply("X00X0")); } + String value = new String("AAAA"); + assertSame(value, Omocode.of(0, new int[] { 1, 2, 4 }).apply(value)); } @Test @@ -61,8 +63,15 @@ public void testNormalize() { public void test_isSameLevel() { assertTrue(Omocode.of(0, new int[0]).isSameLevel(Omocode.of(0, new int[0]))); assertTrue(Omocode.of(1, new int[1]).isSameLevel(Omocode.of(1, new int[1]))); + assertTrue(Omocode.of(0, new int[] {1}).isSameLevel(Omocode.of(0, new int[] {1}))); + assertTrue(Omocode.of(1, new int[] {1}).isSameLevel(Omocode.of(1, new int[] {1}))); + assertFalse(Omocode.of(0, new int[1]).isSameLevel(Omocode.of(0, new int[2]))); + assertFalse(Omocode.of(1, new int[1]).isSameLevel(Omocode.of(0, new int[1]))); + assertFalse(Omocode.of(1, new int[1]).isSameLevel(Omocode.of(1, new int[2]))); assertFalse(Omocode.of(1, new int[] { 1 }).isSameLevel(Omocode.of(1, new int[] { 2 }))); + assertFalse(Omocode.of(0, new int[1]).isSameLevel(Omocode.of(1, new int[2]))); + assertFalse(Omocode.of(0, new int[] { 1 }).isSameLevel(Omocode.of(0, new int[] { 2 }))); } @Test @@ -97,4 +106,26 @@ public void testToOmocodeChar() { assertEquals('M', Omocode.digitToOmocodeChar('M')); } + @Test + public void testWithLevel() { + Omocode uut = Omocode.of(3, new int[] {1,2,3}); + + assertSame(uut, uut.withLevel(3)); + assertEquals(2, uut.withLevel(2).getLevel()); + } + + @Test + public void testIsOmocodeChar() { + assertFalse(Omocode.isOmocodeChar('1')); + assertFalse(Omocode.isOmocodeChar('A')); + assertTrue(Omocode.isOmocodeChar('M')); + } + + @Test + public void testUnsupported() { + Omocode uut = Omocode.unsupported(); + assertEquals(0, uut.getMaxLevel()); + assertEquals(0, uut.getLevel()); + } + } diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/ArrayUtilsTest.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/ArrayUtilsTest.java new file mode 100644 index 0000000..88f3bd8 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/ArrayUtilsTest.java @@ -0,0 +1,27 @@ +package it.kamaladafrica.codicefiscale.utils; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +public class ArrayUtilsTest { + + @Test + public void testImmutableSet() { + Collection collection = Arrays.asList("B", "A", "C"); + + Set set = TestUtils.unmodifiableSet(collection); + + assertNotSame(collection, set); + assertEquals(3, set.size()); + assertThat(set, CoreMatchers.hasItems("A", "B", "C")); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/ArrayUtils_reverseTest.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/ArrayUtils_reverseTest.java new file mode 100644 index 0000000..ea572fd --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/ArrayUtils_reverseTest.java @@ -0,0 +1,39 @@ +package it.kamaladafrica.codicefiscale.utils; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ArrayUtils_reverseTest { + + @Parameter(0) + public int[] array; + + @Parameter(1) + public int[] expected; + + @Test + public void testReverse() { + ArrayUtils.reverse(array); + assertArrayEquals(expected, array); + } + + @Parameters(name="length {index}") + public static Iterable data() { + return Arrays.asList(new Object[][] { + {new int[0], new int[0] }, + {new int[] { 1 }, new int[] { 1 } }, + {new int[] { 1, 2 }, new int[] { 2, 1 } }, + {new int[] { 1, 2, 3 }, new int[] { 3, 2, 1 } }, + {new int[] { 1, 2, 3, 4 }, new int[] { 4, 3, 2, 1 } }, + {new int[] { 1, 3, 2, 4, 6 }, new int[] { 6, 4, 2, 3, 1 } } }); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/PairTest.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/PairTest.java new file mode 100644 index 0000000..6b48cb3 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/PairTest.java @@ -0,0 +1,29 @@ +package it.kamaladafrica.codicefiscale.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class PairTest { + + Pair uut = Pair.of(1, "string"); + + @Test + public void testGetKey() { + assertEquals(Integer.valueOf(1), uut.getKey()); + assertEquals(uut.getLeft(), uut.getKey()); + } + + @Test + public void testGetValue() { + assertEquals("string", uut.getValue()); + assertEquals(uut.getRight(), uut.getValue()); + } + + @Test + public void testSetValue() { + assertThrows(UnsupportedOperationException.class, () -> uut.setValue("newString")); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/RegexUtilsTest.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/RegexUtilsTest.java index 066215b..3477f53 100644 --- a/src/test/java/it/kamaladafrica/codicefiscale/utils/RegexUtilsTest.java +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/RegexUtilsTest.java @@ -1,6 +1,6 @@ package it.kamaladafrica.codicefiscale.utils; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -8,7 +8,7 @@ import org.junit.Test; public class RegexUtilsTest { - + private final String PATTERN = "[A-F1-5]"; private final String TEST = "ABC.defGH0123,45678"; private final String RESULT = "ABC12345"; diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/StringUtilsTest.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/StringUtilsTest.java new file mode 100644 index 0000000..b10a413 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/StringUtilsTest.java @@ -0,0 +1,70 @@ +package it.kamaladafrica.codicefiscale.utils; + +import static it.kamaladafrica.codicefiscale.utils.TestUtils.wrap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class StringUtilsTest { + + @Test + public void testDefaultStringStringString() { + assertEquals(null, StringUtils.defaultString(null, null)); + assertEquals("defaultString", StringUtils.defaultString(null, "defaultString")); + assertEquals("string", StringUtils.defaultString("string", null)); + assertEquals("string", StringUtils.defaultString("string", "defaultString")); + } + + @Test + public void testDefaultStringString() { + assertEquals("", StringUtils.defaultString(null)); + assertEquals("string", StringUtils.defaultString("string")); + } + + @Test + public void testRemoveEnd() { + assertEquals("abab", StringUtils.removeEnd("ababab", "ab")); + assertEquals("ababab", StringUtils.removeEnd("ababab", "ba")); + assertEquals(null, StringUtils.removeEnd(null, "ba")); + assertEquals("", StringUtils.removeEnd("", "ba")); + assertEquals("ababab", StringUtils.removeEnd("ababab", null)); + assertEquals("ababab", StringUtils.removeEnd("ababab", "")); + } + + @Test + public void testReplaceEach() { + assertEquals(null, StringUtils.replaceEach(null, new String[] {"abb", "ccd"}, new String[] {"xx", "yy"})); + assertEquals("", StringUtils.replaceEach("", new String[] {"abb", "ccd"}, new String[] {"xx", "yy"})); + assertEquals("aabbccdd", StringUtils.replaceEach("aabbccdd", null, null)); + assertEquals("aabbccdd", StringUtils.replaceEach("aabbccdd", new String[] {"aa"}, null)); + assertEquals("aabbccdd", StringUtils.replaceEach("aabbccdd", null, new String[] {"aa"})); + assertEquals("aabbccdd", StringUtils.replaceEach("aabbccdd", new String[] {"aa"}, new String[] {null})); + assertEquals("aabbccdd", StringUtils.replaceEach("aabbccdd", new String[] {null}, new String[] {"aa"})); + assertEquals("wxyzz", StringUtils.replaceEach("aabbccdd", new String[] {"aa", "bb", "cc", "d"}, new String[] {"w", "x", "y", "z"})); + assertEquals("xbbzdd", StringUtils.replaceEach("aabbccdd", new String[] {"aa", "bb", "cc"}, new String[] {"x", null, "z"})); + assertEquals("xbbzdd", StringUtils.replaceEach("aabbccdd", new String[] {"aa", null, "cc"}, new String[] {"x", "y", "z"})); + assertEquals("axxaxxyydyyd", StringUtils.replaceEach("aabbaabbccddccdd", new String[] {"abb", "ccd"}, new String[] {"xx", "yy"})); + + assertThrows(IllegalArgumentException.class, () -> StringUtils.replaceEach("string", new String[] {"a"}, new String[] {"a", "b"})); + } + + @Test + public void testEquals() { + String s = "aaa"; + assertTrue(StringUtils.equals(wrap(s), s)); + assertFalse(StringUtils.equals(null, s)); + assertFalse(StringUtils.equals(wrap(s), null)); + assertTrue(StringUtils.equals(null, null)); + assertFalse(StringUtils.equals(wrap("aaa"), "aaaa")); + assertFalse(StringUtils.equals("aaa", "bbb")); + assertTrue(StringUtils.equals(new String("aaa"), new String("aaa"))); + assertTrue(StringUtils.equals(wrap(new String("aaa")), new String("aaa"))); + assertFalse(StringUtils.equals(wrap(new String("aaa")), new String("bbb"))); + assertFalse(StringUtils.equals(wrap(new String("aaa")), wrap(new String("bbb")))); + assertTrue(StringUtils.equals(wrap(new String("aaa")), wrap(new String("aaa")))); + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/TestUtils.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/TestUtils.java new file mode 100644 index 0000000..55541ef --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/TestUtils.java @@ -0,0 +1,44 @@ +package it.kamaladafrica.codicefiscale.utils; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class TestUtils { + + public static Set unmodifiableSet(Collection collection) { + return Collections.unmodifiableSet(new LinkedHashSet<>(collection)); + } + + /** + * Wrap the string to a {@link CharSequence}. This ensures that using the + * {@link Object#equals(Object)} method on the input CharSequence to test for + * equality will fail. + * + * @param string the string + * @return the char sequence + */ + public static CharSequence wrap(final String string) { + return new CharSequence() { + @Override + public char charAt(final int index) { + return string.charAt(index); + } + + @Override + public int length() { + return string.length(); + } + + @Override + public CharSequence subSequence(final int start, final int end) { + return string.subSequence(start, end); + } + }; + } + +} diff --git a/src/test/java/it/kamaladafrica/codicefiscale/utils/ValidateTest.java b/src/test/java/it/kamaladafrica/codicefiscale/utils/ValidateTest.java new file mode 100644 index 0000000..760b0d5 --- /dev/null +++ b/src/test/java/it/kamaladafrica/codicefiscale/utils/ValidateTest.java @@ -0,0 +1,84 @@ +package it.kamaladafrica.codicefiscale.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class ValidateTest { + + @Test + public void testNotEmptyTStringObjectArray() { + Exception e = assertThrows(IllegalArgumentException.class, () -> Validate.notEmpty("", "message: %s", "arg")); + assertEquals("message: arg", e.getMessage()); + + assertThrows(NullPointerException.class, () -> Validate.notEmpty(null, "message: %s", "arg")); + + Validate.notEmpty("string", "message: %s", "arg"); + } + + @Test + public void testNotEmptyT() { + assertThrows(IllegalArgumentException.class, () -> Validate.notEmpty("")); + assertThrows(NullPointerException.class, () -> Validate.notEmpty(null)); + Validate.notEmpty("string"); + } + + @Test + public void testMatchesPatternTStringStringObjectArray() { + Exception e = assertThrows(IllegalArgumentException.class, + () -> Validate.matchesPattern("string", "x*", "message: %s", "arg")); + assertEquals("message: arg", e.getMessage()); + + assertThrows(NullPointerException.class, () -> Validate.matchesPattern(null, "x*", "message: %s", "arg")); + + Validate.matchesPattern("string", "\\wtr.*", "message: %s", "arg"); + } + + @Test + public void testMatchesPatternTString() { + assertThrows(IllegalArgumentException.class, () -> Validate.matchesPattern("string", "x*")); + assertThrows(NullPointerException.class, () -> Validate.matchesPattern(null, "x*")); + + Validate.matchesPattern("string", "\\wtr.*"); + } + + @Test + public void testInclusiveBetweenIntIntIntStringObjectArray() { + Exception e = assertThrows(IllegalArgumentException.class, + () -> Validate.inclusiveBetween(1, 3, 5, "message: %s", "arg")); + assertEquals("message: arg", e.getMessage()); + + Validate.inclusiveBetween(1, 3, 2, "message: %s", "arg"); + Validate.inclusiveBetween(0, 0, 0, "message: %s", "arg"); + } + + @Test + public void testInclusiveBetweenIntIntInt() { + assertThrows(IllegalArgumentException.class, () -> Validate.inclusiveBetween(1, 3, 5)); + + Validate.inclusiveBetween(1, 3, 2); + Validate.inclusiveBetween(0, 0, 0); + } + + @Test + public void testValidIndexTIntStringObjectArray() { + Exception e = assertThrows(IllegalArgumentException.class, + () -> Validate.validIndex("abc", 3, "message: %s", "arg")); + assertEquals("message: arg", e.getMessage()); + assertThrows(NullPointerException.class, () -> Validate.validIndex(null, 2)); + + Validate.validIndex("abc", 2, "message: %s", "arg"); + Validate.validIndex("abc", 0, "message: %s", "arg"); + } + + @Test + public void testValidIndexTInt() { + assertThrows(IllegalArgumentException.class, () -> Validate.validIndex("abc", 3)); + assertThrows(NullPointerException.class, () -> Validate.validIndex(null, 2)); + + Validate.validIndex("abc", 2); + Validate.validIndex("abc", 0); + } + +}