View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.language;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  
23  import java.util.stream.Stream;
24  
25  import org.apache.commons.codec.AbstractStringEncoderTest;
26  import org.junit.jupiter.api.Test;
27  import org.junit.jupiter.params.ParameterizedTest;
28  import org.junit.jupiter.params.provider.Arguments;
29  import org.junit.jupiter.params.provider.CsvSource;
30  import org.junit.jupiter.params.provider.MethodSource;
31  import org.junit.jupiter.params.provider.ValueSource;
32  
33  /**
34   * Tests {@link Nysiis}
35   */
36  class NysiisTest extends AbstractStringEncoderTest<Nysiis> {
37  
38      static Stream<Arguments> dropByCases() {
39          // Explanation of differences between this implementation and the one at dropby.com is
40          // prepended to the test string. The referenced rules refer to the outlined steps the
41          // class description for Nysiis.
42          // @formatter:off
43          return Stream.of(
44                  // 1. Transcode first characters of name
45                  Arguments.of("MACINTOSH", "MCANT"),
46                  // violates 4j: the second N should not be added, as the first
47                  //              key char is already a N
48                  Arguments.of("KNUTH", "NAT"),           // Original: NNAT; modified: NATH
49                  // O and E are transcoded to A because of rule 4a
50                  // H also to A because of rule 4h
51                  // the N gets mysteriously lost, maybe because of a wrongly implemented rule 4h
52                  // that skips the next char in such a case?
53                  // the remaining A is removed because of rule 7
54                  Arguments.of("KOEHN", "CAN"),           // Original: C
55                  // violates 4j: see also KNUTH
56                  Arguments.of("PHILLIPSON", "FALAPSAN"), // Original: FFALAP[SAN]
57                  // violates 4j: see also KNUTH
58                  Arguments.of("PFEISTER", "FASTAR"),     // Original: FFASTA[R]
59                  // violates 4j: see also KNUTH
60                  Arguments.of("SCHOENHOEFT", "SANAFT"),  // Original: SSANAF[T]
61                  // 2. Transcode last characters of name:
62                  Arguments.of("MCKEE", "MCY"),
63                  Arguments.of("MACKIE", "MCY"),
64                  Arguments.of("HEITSCHMIDT", "HATSNAD"),
65                  Arguments.of("BART", "BAD"),
66                  Arguments.of("HURD", "HAD"),
67                  Arguments.of("HUNT", "HAD"),
68                  Arguments.of("WESTERLUND", "WASTARLAD"),
69                  // 4. Transcode remaining characters by following these rules,
70                  //    incrementing by one character each time:
71                  Arguments.of("CASSTEVENS", "CASTAFAN"),
72                  Arguments.of("VASQUEZ", "VASG"),
73                  Arguments.of("FRAZIER", "FRASAR"),
74                  Arguments.of("BOWMAN", "BANAN"),
75                  Arguments.of("MCKNIGHT", "MCNAGT"),
76                  Arguments.of("RICKERT", "RACAD"),
77                  // violates 5: the last S is not removed
78                  // when comparing to DEUTS, which is phonetically similar
79                  // the result it also DAT, which is correct for DEUTSCH too imo
80                  Arguments.of("DEUTSCH", "DAT"),         // Original: DATS
81                  Arguments.of("WESTPHAL", "WASTFAL"),
82                  // violates 4h: the H should be transcoded to S and thus ignored as
83                  // the first key character is also S
84                  Arguments.of("SHRIVER", "SRAVAR"),      // Original: SHRAVA[R]
85                  // same as KOEHN, the L gets mysteriously lost
86                  Arguments.of("KUHL", "CAL"),            // Original: C
87                  Arguments.of("RAWSON", "RASAN"),
88                  // If last character is S, remove it
89                  Arguments.of("JILES", "JAL"),
90                  // violates 6: if the last two characters are AY, remove A
91                  Arguments.of("CARRAWAY", "CARY"),       // Original: CARAY
92                  Arguments.of("YAMADA", "YANAD"));
93          // @formatter:on
94      }
95  
96      static Stream<Arguments> othersCases() {
97          // @formatter:off
98          return Stream.of(
99                  Arguments.of("O'Daniel", "ODANAL"),
100                 Arguments.of("O'Donnel", "ODANAL"),
101                 Arguments.of("Cory", "CARY"),
102                 Arguments.of("Corey", "CARY"),
103                 Arguments.of("Kory", "CARY"),
104                 //
105                 Arguments.of("FUZZY", "FASY"));
106         // @formatter:on
107     }
108 
109     static Stream<Arguments> rule1Cases() {
110         // @formatter:off
111         return Stream.of(
112                 Arguments.of("MACX", "MCX"),
113                 Arguments.of("KNX", "NX"),
114                 Arguments.of("KX", "CX"),
115                 Arguments.of("PHX", "FX"),
116                 Arguments.of("PFX", "FX"),
117                 Arguments.of("SCHX", "SX"));
118         // @formatter:on
119     }
120 
121     static Stream<Arguments> rule2Cases() {
122         // @formatter:off
123         return Stream.of(
124                 Arguments.of("XEE", "XY"),
125                 Arguments.of("XIE", "XY"),
126                 Arguments.of("XDT", "XD"),
127                 Arguments.of("XRT", "XD"),
128                 Arguments.of("XRD", "XD"),
129                 Arguments.of("XNT", "XD"),
130                 Arguments.of("XND", "XD"));
131         // @formatter:on
132     }
133 
134     static Stream<Arguments> rule4Dot1Cases() {
135         // @formatter:off
136         return Stream.of(
137                 Arguments.of("XEV", "XAF"),
138                 Arguments.of("XAX", "XAX"),
139                 Arguments.of("XEX", "XAX"),
140                 Arguments.of("XIX", "XAX"),
141                 Arguments.of("XOX", "XAX"),
142                 Arguments.of("XUX", "XAX"));
143         // @formatter:on
144     }
145 
146     static Stream<Arguments> rule4Dot2Cases() {
147         // @formatter:off
148         return Stream.of(
149                 Arguments.of("XQ", "XG"),
150                 Arguments.of("XZ", "X"),
151                 Arguments.of("XM", "XN"));
152         // @formatter:on
153     }
154 
155     static Stream<Arguments> rule5Cases() {
156         // @formatter:off
157         return Stream.of(
158                 Arguments.of("XS", "X"),
159                 Arguments.of("XSS", "X"));
160         // @formatter:on
161     }
162 
163     static Stream<Arguments> rule6Cases() {
164         // @formatter:off
165         return Stream.of(
166                 Arguments.of("XAY", "XY"),
167                 Arguments.of("XAYS", "XY")); // Rules 5, 6
168         // @formatter:on
169     }
170 
171     static Stream<Arguments> rule7Cases() {
172         // @formatter:off
173         return Stream.of(
174                 Arguments.of("XA", "X"),
175                 Arguments.of("XAS", "X")); // Rules 5, 7
176         // @formatter:on
177     }
178 
179     private final Nysiis fullNysiis = new Nysiis(false);
180 
181     @Override
182     protected Nysiis createStringEncoder() {
183         return new Nysiis();
184     }
185 
186     @ParameterizedTest(name = "{0}")
187     @ValueSource(strings = { "Brian", "Brown", "Brun" })
188     void testBran(final String input) {
189         assertEquals("BRAN", getStringEncoder().encode(input));
190     }
191 
192     @ParameterizedTest(name = "{0}")
193     @ValueSource(strings = { "Capp", "Cope", "Copp", "Kipp" })
194     void testCap(final String input) {
195         assertEquals("CAP", getStringEncoder().encode(input));
196     }
197 
198     @Test
199     void testDad() {
200         // Data Quality and Record Linkage Techniques P.121 claims this is DAN,
201         // but it should be DAD, verified also with dropby.com
202         assertEquals("DAD", getStringEncoder().encode("Dent"));
203     }
204 
205     @ParameterizedTest(name = "{0}")
206     @ValueSource(strings = { "Dane", "Dean", "Dionne" })
207     void testDan(final String input) {
208         assertEquals("DAN", getStringEncoder().encode(input));
209     }
210 
211     /**
212      * Tests data gathered from around the internet.
213      *
214      * @see <a href="https://www.dropby.com/NYSIISTextStrings.html">http://www.dropby.com/NYSIISTextStrings.html</a>
215      */
216     @ParameterizedTest(name = "{0}: {1}")
217     @MethodSource("dropByCases")
218     void testDropBy(final String input, final String expected) {
219         assertEquals(expected, fullNysiis.encode(input));
220     }
221 
222     @Test
223     void testFal() {
224         assertEquals("FAL", getStringEncoder().encode("Phil"));
225     }
226 
227     /**
228      * Tests data gathered from around the internets.
229      */
230     @ParameterizedTest(name = "{0}: {1}")
231     @MethodSource("othersCases")
232     void testOthers(final String input, final String expected) {
233         assertEquals(expected, fullNysiis.encode(input));
234     }
235 
236     /**
237      * Tests rule 1: Translate first characters of name: MAC → MCC, KN → N, K → C, PH, PF → FF, SCH → SSS
238      */
239     @ParameterizedTest(name = "{0}: {1}")
240     @MethodSource("rule1Cases")
241     void testRule1(final String input, final String expected) {
242         assertEquals(expected, fullNysiis.encode(input));
243     }
244 
245     /**
246      * Tests rule 2: Translate last characters of name: EE → Y, IE → Y, DT, RT, RD, NT, ND → D
247      */
248     @ParameterizedTest(name = "{0}: {1}")
249     @MethodSource("rule2Cases")
250     void testRule2(final String input, final String expected) {
251         assertEquals(expected, fullNysiis.encode(input));
252     }
253 
254     /**
255      * Tests rule 4.1: EV → AF else A, E, I, O, U → A
256      */
257     @ParameterizedTest(name = "{0}: {1}")
258     @MethodSource("rule4Dot1Cases")
259     void testRule4Dot1(final String input, final String expected) {
260         assertEquals(expected, fullNysiis.encode(input));
261     }
262 
263     /**
264      * Tests rule 4.2: Q → G, Z → S, M → N
265      */
266     @ParameterizedTest(name = "{0}: {1}")
267     @MethodSource("rule4Dot2Cases")
268     void testRule4Dot2(final String input, final String expected) {
269         assertEquals(expected, fullNysiis.encode(input));
270     }
271 
272     /**
273      * Tests rule 5: If last character is S, remove it.
274      */
275     @ParameterizedTest(name = "{0}: {1}")
276     @MethodSource("rule5Cases")
277     void testRule5(final String input, final String expected) {
278         assertEquals(expected, fullNysiis.encode(input));
279     }
280 
281     /**
282      * Tests rule 6: If last characters are AY, replace with Y.
283      */
284     @ParameterizedTest(name = "{0}: {1}")
285     @MethodSource("rule6Cases")
286     void testRule6(final String input, final String expected) {
287         assertEquals(expected, fullNysiis.encode(input));
288     }
289 
290     /**
291      * Tests rule 7: If last character is A, remove it.
292      */
293     @ParameterizedTest(name = "{0}: {1}")
294     @MethodSource("rule7Cases")
295     void testRule7(final String input, final String expected) {
296         assertEquals(expected, fullNysiis.encode(input));
297     }
298 
299     @Test
300     void testSnad() {
301         // Data Quality and Record Linkage Techniques P.121 claims this is SNAT,
302         // but it should be SNAD
303         assertEquals("SNAD", getStringEncoder().encode("Schmidt"));
304     }
305 
306     @ParameterizedTest(name = "{0}")
307     @ValueSource(strings = { "Smith", "Schmit" })
308     void testSnat(final String input) {
309         assertEquals("SNAT", getStringEncoder().encode(input));
310     }
311 
312     @ParameterizedTest(name = "{0}: {1}")
313     @CsvSource({
314         "Kobwick, CABWAC",
315         "Kocher, CACAR",
316         "Fesca, FASC",
317         "Shom, SAN",
318         "Ohlo, OL",
319         "Uhu, UH",
320         "Um, UN"
321     })
322     void testSpecialBranches(final String input, final String expected) {
323         assertEquals(expected, getStringEncoder().encode(input));
324     }
325 
326     @ParameterizedTest(name = "{0}")
327     @ValueSource(strings = { "Trueman", "Truman" })
328     void testTranan(final String input) {
329         assertEquals("TRANAN", getStringEncoder().encode(input));
330     }
331 
332     @Test
333     void testTrueVariant() {
334         final Nysiis encoder = new Nysiis(true);
335 
336         final String encoded = encoder.encode("WESTERLUND");
337         assertTrue(encoded.length() <= 6);
338         assertEquals("WASTAR", encoded);
339     }
340 
341 }