Repository: commons-text Updated Branches: refs/heads/master 07f4dd9a8 -> c05e0251a
TEXT-19 Add alphabet converter closes #5 Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/c05e0251 Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/c05e0251 Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/c05e0251 Branch: refs/heads/master Commit: c05e0251a0178f8544696e3d1451826b4d66cc40 Parents: 07f4dd9 Author: eallweil <eallw...@paypal.com> Authored: Mon Sep 19 09:51:00 2016 +0300 Committer: Bruno P. Kinoshita <brunodepau...@yahoo.com.br> Committed: Sun Oct 2 20:14:13 2016 +1300 ---------------------------------------------------------------------- .../apache/commons/text/AlphabetConverter.java | 436 +++++++++++++++++++ .../commons/text/AlphabetConverterTest.java | 204 +++++++++ 2 files changed, 640 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-text/blob/c05e0251/src/main/java/org/apache/commons/text/AlphabetConverter.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/text/AlphabetConverter.java b/src/main/java/org/apache/commons/text/AlphabetConverter.java new file mode 100644 index 0000000..5fc3528 --- /dev/null +++ b/src/main/java/org/apache/commons/text/AlphabetConverter.java @@ -0,0 +1,436 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +/** + * <p> + * Convert from one alphabet to another, with the possibility of leaving certain characters unencoded. + * </p> + * + * <p> + * The target and do not encode languages must be in the Unicode BMP, but the source language does not. + * </p> + * + * <p> + * The encoding will all be of a fixed length, except for the 'do not encode' chars, which will be of length 1 + * </p> + * + * <h3>Sample usage</h3> + * + * <pre> + * Character[] originals; // a, b, c, d + * Character[] encoding; // 0, 1, d + * Character[] doNotEncode; // d + * + * AlphabetConverter ac = AlphabetConverter.createConverterFromChars(originals, encoding, doNotEncode); + * + * ac.encode("a"); // 00 + * ac.encode("b"); // 01 + * ac.encode("c"); // 0d + * ac.encode("d"); // d + * ac.encode("abcd"); // 00010dd + * </pre> + * + * <p> + * #ThreadSafe# AlphabetConverter class methods are threadsafe as they do not change internal state. + * </p> + * + * @since 0.1 + */ +public class AlphabetConverter { + + private final Map<Integer, String> originalToEncoded; + private final Map<String, String> encodedToOriginal; + + private final int encodedLetterLength; + + private static final String ARROW = " -> "; + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + /** + * Hidden constructor for alphabet converter. Used by static helper methods. + * + * @param originalToEncoded original string to be encoded + * @param encodedToOriginal encoding alphabet + * @param doNotEncodeMap encoding black list + * @param encodedLetterLength length of the encoded letter + */ + private AlphabetConverter(Map<Integer, String> originalToEncoded, Map<String, String> encodedToOriginal, + Map<Integer, String> doNotEncodeMap, int encodedLetterLength) { + + this.originalToEncoded = originalToEncoded; + this.encodedToOriginal = encodedToOriginal; + this.encodedLetterLength = encodedLetterLength; + } + + /** + * Encode a given string. + * + * @param original the string to be encoded + * @return the encoded string, {@code null} if the given string is null + * @throws UnsupportedEncodingException if chars that are not supported are encountered + */ + public String encode(String original) throws UnsupportedEncodingException { + if (original == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < original.length();) { + int codepoint = original.codePointAt(i); + + String nextLetter = originalToEncoded.get(codepoint); + + if (nextLetter == null) { + throw new UnsupportedEncodingException( + "Couldn't find encoding for '" + codePointToString(codepoint) + "' in " + original); + } + + sb.append(nextLetter); + + i += Character.charCount(codepoint); + } + + return sb.toString(); + } + + /** + * Decodes a given string + * + * @param encoded a string that has been encoded using this AlphabetConverter + * @return the decoded string, {@code null} if the given string is null + * @throws UnsupportedEncodingException if unexpected characters that cannot be handled are encountered + */ + public String decode(String encoded) throws UnsupportedEncodingException { + if (encoded == null) { + return null; + } + + StringBuilder result = new StringBuilder(); + + for (int j = 0; j < encoded.length();) { + Integer i = encoded.codePointAt(j); + String s = codePointToString(i); + + if (s.equals(originalToEncoded.get(i))) { + result.append(s); + j++; // because we do not encode in Unicode extended the length of each encoded char is 1 + } else { + if (j + encodedLetterLength > encoded.length()) { + throw new UnsupportedEncodingException("Unexpected end of string while decoding " + encoded); + } else { + String nextGroup = encoded.substring(j, j + encodedLetterLength); + String next = encodedToOriginal.get(nextGroup); + if (next == null) { + throw new UnsupportedEncodingException( + "Unexpected string without decoding (" + nextGroup + ") in " + encoded); + } else { + result.append(next); + j += encodedLetterLength; + } + } + } + } + + return result.toString(); + } + + /** + * Get the length of characters in the encoded alphabet that are necessary for each character in the original + * alphabet. + * + * @return the length of the encoded char + */ + public int getEncodedCharLength() { + return encodedLetterLength; + } + + /** + * Get the mapping from integer code point of source language to encoded string. Use to reconstruct converter from + * serialized map + * + * @return the original map + */ + public Map<Integer, String> getOriginalToEncoded() { + return Collections.unmodifiableMap(originalToEncoded); + } + + /** + * Recursive method used when creating encoder/decoder + */ + private void addSingleEncoding(int level, String currentEncoding, Collection<Integer> encoding, + Iterator<Integer> originals, Map<Integer, String> doNotEncodeMap) { + + if (level > 0) { + for (int encodingLetter : encoding) { + if (originals.hasNext()) { + + // this skips the doNotEncode chars if they are in the + // leftmost place + if (level != encodedLetterLength || !doNotEncodeMap.containsKey(encodingLetter)) { + addSingleEncoding(level - 1, currentEncoding + codePointToString(encodingLetter), encoding, + originals, doNotEncodeMap); + } + } else { + return; // done encoding all the original alphabet + } + } + } else { + Integer next = originals.next(); + + while (doNotEncodeMap.containsKey(next)) { + String originalLetterAsString = codePointToString(next); + + originalToEncoded.put(next, originalLetterAsString); + encodedToOriginal.put(originalLetterAsString, originalLetterAsString); + + if (!originals.hasNext()) { + return; + } + + next = originals.next(); + } + + String originalLetterAsString = codePointToString(next); + + originalToEncoded.put(next, currentEncoding); + encodedToOriginal.put(currentEncoding, originalLetterAsString); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (Entry<Integer, String> entry : originalToEncoded.entrySet()) { + sb.append(codePointToString(entry.getKey())).append(ARROW).append(entry.getValue()).append(LINE_SEPARATOR); + } + + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj instanceof AlphabetConverter == false) { + return false; + } + final AlphabetConverter other = (AlphabetConverter) obj; + return originalToEncoded.equals(other.originalToEncoded) && encodedToOriginal.equals(other.encodedToOriginal) + && encodedLetterLength == other.encodedLetterLength; + } + + @Override + public int hashCode() { + return Objects.hash(originalToEncoded, encodedToOriginal, encodedLetterLength); + } + + // -- static methods + + /** + * Create a new converter from a map. + * + * @param originalToEncoded a map returned from getOriginalToEncoded() + * @return the reconstructed AlphabetConverter + * @see AlphabetConverter#getOriginalToEncoded() + */ + public static AlphabetConverter createConverterFromMap(Map<Integer, String> originalToEncoded) { + final Map<Integer, String> unmodifiableOriginalToEncoded = Collections.unmodifiableMap(originalToEncoded); + Map<String, String> encodedToOriginal = new LinkedHashMap<>(); + Map<Integer, String> doNotEncodeMap = new HashMap<>(); + + int encodedLetterLength = 1; + + for (Entry<Integer, String> e : unmodifiableOriginalToEncoded.entrySet()) { + String originalAsString = codePointToString(e.getKey()); + encodedToOriginal.put(e.getValue(), originalAsString); + + if (e.getValue().equals(originalAsString)) { + doNotEncodeMap.put(e.getKey(), e.getValue()); + } + + if (e.getValue().length() > encodedLetterLength) { + encodedLetterLength = e.getValue().length(); + } + } + + return new AlphabetConverter(unmodifiableOriginalToEncoded, encodedToOriginal, doNotEncodeMap, + encodedLetterLength); + } + + /** + * Create an alphabet converter, for converting from the original alphabet, to the encoded alphabet, while leaving + * the characters in <em>doNotEncode</em> as they are (if possible). + * + * Duplicate letters in either original or encoding will be ignored. + * + * @param original an array of chars representing the original alphabet + * @param encoding an array of chars representing the alphabet to be used for encoding + * @param doNotEncode an array of chars to be encoded using the original alphabet - every char here must appear in + * both the previous params + * @return the AlphabetConverter + * @throws IllegalArgumentException if an AlphabetConverter cannot be constructed + */ + public static AlphabetConverter createConverterFromChars(Character[] original, Character[] encoding, + Character[] doNotEncode) { + return AlphabetConverter.createConverter(convertCharsToIntegers(original), convertCharsToIntegers(encoding), + convertCharsToIntegers(doNotEncode)); + } + + private static Integer[] convertCharsToIntegers(Character[] chars) { + if (chars == null || chars.length == 0) { + return new Integer[0]; + } + Integer[] integers = new Integer[chars.length]; + for (int i = 0; i < chars.length; i++) { + integers[i] = (int) chars[i]; + } + return integers; + } + + /** + * Create an alphabet converter, for converting from the original alphabet, to the encoded alphabet, while leaving + * the characters in <em>doNotEncode</em> as they are (if possible) + * + * Duplicate letters in either original or encoding will be ignored + * + * @param original an array of ints representing the original alphabet in codepoints + * @param encoding an array of ints representing the alphabet to be used for encoding, in codepoints + * @param doNotEncode an array of ints representing the chars to be encoded using the original alphabet - every char + * here must appear in both the previous params + * @return the AlphabetConverter + * @throws IllegalArgumentException if an AlphabetConverter cannot be constructed + */ + public static AlphabetConverter createConverter(Integer[] original, Integer[] encoding, Integer[] doNotEncode) { + + Set<Integer> originalCopy = new LinkedHashSet<>(Arrays.<Integer> asList(original)); + Set<Integer> encodingCopy = new LinkedHashSet<>(Arrays.<Integer> asList(encoding)); + Set<Integer> doNotEncodeCopy = new LinkedHashSet<>(Arrays.<Integer> asList(doNotEncode)); + + final Map<Integer, String> originalToEncoded = new LinkedHashMap<>(); + final Map<String, String> encodedToOriginal = new LinkedHashMap<>(); + final Map<Integer, String> doNotEncodeMap = new HashMap<>(); + + int encodedLetterLength; + + for (int i : doNotEncodeCopy) { + if (!originalCopy.contains(i)) { + throw new IllegalArgumentException( + "Can not use 'do not encode' list because original alphabet does not contain '" + + codePointToString(i) + "'"); + } + + if (!encodingCopy.contains(i)) { + throw new IllegalArgumentException( + "Can not use 'do not encode' list because encoding alphabet does not contain '" + + codePointToString(i) + "'"); + } + + doNotEncodeMap.put(i, codePointToString(i)); + } + + if (encodingCopy.size() >= originalCopy.size()) { + encodedLetterLength = 1; + + Iterator<Integer> it = encodingCopy.iterator(); + + for (int originalLetter : originalCopy) { + String originalLetterAsString = codePointToString(originalLetter); + + if (doNotEncodeMap.containsKey(originalLetter)) { + originalToEncoded.put(originalLetter, originalLetterAsString); + encodedToOriginal.put(originalLetterAsString, originalLetterAsString); + } else { + Integer next = it.next(); + + while (doNotEncodeCopy.contains(next)) { + next = it.next(); + } + + String encodedLetter = codePointToString(next); + + originalToEncoded.put(originalLetter, encodedLetter); + encodedToOriginal.put(encodedLetter, originalLetterAsString); + } + } + + return new AlphabetConverter(originalToEncoded, encodedToOriginal, doNotEncodeMap, encodedLetterLength); + + } else if (encodingCopy.size() - doNotEncodeCopy.size() < 2) { + throw new IllegalArgumentException( + "Must have at least two encoding characters (not counting those in the 'do not encode' list), but has " + + (encodingCopy.size() - doNotEncodeCopy.size())); + } else { + // we start with one which is our minimum, and because we do the + // first division outside the loop + int lettersSoFar = 1; + + // the first division takes into account that the doNotEncode + // letters can't be in the leftmost place + int lettersLeft = (originalCopy.size() - doNotEncodeCopy.size()) + / (encodingCopy.size() - doNotEncodeCopy.size()); + + while (lettersLeft / encodingCopy.size() >= 1) { + lettersLeft = lettersLeft / encodingCopy.size(); + lettersSoFar++; + } + + encodedLetterLength = lettersSoFar + 1; + + AlphabetConverter ac = new AlphabetConverter(originalToEncoded, encodedToOriginal, doNotEncodeMap, + encodedLetterLength); + + ac.addSingleEncoding(encodedLetterLength, "", encodingCopy, originalCopy.iterator(), doNotEncodeMap); + + return ac; + } + } + + /** + * Create new String that contains just the given code point. + * + * @param i code point + * @return a new string with the new code point + * @see http://www.oracle.com/us/technologies/java/supplementary-142654.html + */ + private static String codePointToString(int i) { + if (Character.charCount(i) == 1) { + return String.valueOf((char) i); + } else { + return new String(Character.toChars(i)); + } + } +} http://git-wip-us.apache.org/repos/asf/commons-text/blob/c05e0251/src/test/java/org/apache/commons/text/AlphabetConverterTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/text/AlphabetConverterTest.java b/src/test/java/org/apache/commons/text/AlphabetConverterTest.java new file mode 100644 index 0000000..94c4a30 --- /dev/null +++ b/src/test/java/org/apache/commons/text/AlphabetConverterTest.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Unit tests for {@link org.apache.commons.lang3.text.AlphabetConverter}. + */ +public class AlphabetConverterTest { + + private static Character[] lower_case_english = {' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'}; + private static Character[] english_and_numbers = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',' ' }; + private static Character[] lower_case_english_and_numbers = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',' ' }; + private static Character[] numbers = {'0','1','2','3','4','5','6','7','8','9'}; + private static Character[] binary = {'0','1'}; + private static Character[] hebrew = {'_', ' ', '\u05e7','\u05e8','\u05d0','\u05d8','\u05d5','\u05df','\u05dd','\u05e4','\u05e9','\u05d3','\u05d2','\u05db','\u05e2','\u05d9','\u05d7','\u05dc','\u05da','\u05e3','\u05d6','\u05e1','\u05d1','\u05d4','\u05e0','\u05de','\u05e6','\u05ea','\u05e5'}; + private static Character[] empty = {}; + + private static Integer[] unicode = {32,35395,35397,36302,36291,35203,35201,35215,35219,35268,97,98,99,100,101,102,103,104,105,106,107,108,109,110,1001,1002,1003,1004,1005}; + private static Integer[] lower_case_english_codepoints = {32,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122}; + private static Integer[] doNotEncodePoints = {32,97,98,99}; // space, a, b, c + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void encodeFailureTest() throws UnsupportedEncodingException { + thrown.expect(UnsupportedEncodingException.class); + thrown.expectMessage("Couldn't find encoding for '3'"); + test(binary, numbers, empty, "3"); + } + + @Test + public void binaryTest() throws UnsupportedEncodingException { + test(binary, numbers, empty, "0", "1", "10", "11"); + test(numbers, binary, empty, "12345", "0"); + test(lower_case_english, binary, empty, "abc", "a"); + } + + @Test + public void hebrewTest() throws UnsupportedEncodingException { + test(hebrew, binary, empty, "\u05d0", "\u05e2", "\u05d0\u05dc\u05e3_\u05d0\u05d5\u05d4\u05d1\u05dc_\u05d1\u05d9\u05ea_\u05d6\u05d4_\u05d1\u05d9\u05ea_\u05d2\u05d9\u05de\u05dc_\u05d6\u05d4_\u05db\u05de\u05dc_\u05d2\u05d3\u05d5\u05dc"); + test(hebrew, numbers, empty, "\u05d0", "\u05e2", "\u05d0\u05dc\u05e3_\u05d0\u05d5\u05d4\u05d1\u05dc_\u05d1\u05d9\u05ea_\u05d6\u05d4_\u05d1\u05d9\u05ea_\u05d2\u05d9\u05de\u05dc_\u05d6\u05d4_\u05db\u05de\u05dc_\u05d2\u05d3\u05d5\u05dc"); + test(numbers, hebrew, empty, "123456789", "1", "5"); + test(lower_case_english, hebrew, empty, "this is a test"); + } + + @Test + public void doNotEncodeTest() throws UnsupportedEncodingException { + test(english_and_numbers, lower_case_english_and_numbers, lower_case_english, "1", "456", "abc", "ABC", "this will not be converted but THIS WILL"); + test(english_and_numbers, lower_case_english_and_numbers, numbers, "1", "456", "abc", "ABC", "this will be converted but 12345 and this will be"); + } + + private AlphabetConverter createJavadocExample() { + Character[] original = {'a','b','c','d'}; + Character[] encoding = {'0','1','d'}; + Character[] doNotEncode = {'d'}; + + return AlphabetConverter.createConverterFromChars(original, encoding, doNotEncode); + } + + /* + * Test example in javadocs for consistency + */ + @Test + public void javadocExampleTest() throws UnsupportedEncodingException { + AlphabetConverter ac = createJavadocExample(); + + Assert.assertEquals("00", ac.encode("a")); + Assert.assertEquals("01", ac.encode("b")); + Assert.assertEquals("0d", ac.encode("c")); + Assert.assertEquals("d", ac.encode("d")); + Assert.assertEquals("00010dd", ac.encode("abcd")); + } + + @Test + public void unexpectedEndwhileDecodingTest() throws UnsupportedEncodingException { + String toDecode = "00d01d0"; + + thrown.expect(UnsupportedEncodingException.class); + thrown.expectMessage("Unexpected end of string while decoding " + toDecode); + + AlphabetConverter ac = createJavadocExample(); + ac.decode(toDecode); + } + + @Test + public void unexpectedStringWhileDecodingTest() throws UnsupportedEncodingException { + String toDecode = "00XX"; + + thrown.expect(UnsupportedEncodingException.class); + thrown.expectMessage("Unexpected string without decoding (XX) in " + toDecode); + + AlphabetConverter ac = createJavadocExample(); + ac.decode(toDecode); + } + + /* + * Test constructor from code points + */ + @Test + public void unicodeTest() throws UnsupportedEncodingException { + AlphabetConverter ac = AlphabetConverter.createConverter(unicode, lower_case_english_codepoints, doNotEncodePoints); + + Assert.assertEquals(2, ac.getEncodedCharLength()); + + String original = "\u8a43\u8a45 \u8dce ab \u8dc3 c \u8983"; + String encoded = ac.encode(original); + String decoded = ac.decode(encoded); + + Assert.assertEquals("Encoded '" + original + "' into '" + encoded + "', but decoded into '" + decoded + "'", original, decoded); + } + + @Test + public void noEncodingLettersTest() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Must have at least two encoding characters (not counting those in the 'do not encode' list), but has 0"); + + AlphabetConverter.createConverterFromChars(english_and_numbers, numbers, numbers); + } + + @Test + public void onlyOneEncodingLettersTest() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Must have at least two encoding characters (not counting those in the 'do not encode' list), but has 1"); + + Character[] numbersPlusUnderscore = Arrays.copyOf(numbers, numbers.length + 1); + numbersPlusUnderscore[numbersPlusUnderscore.length -1] = '_'; + + AlphabetConverter.createConverterFromChars(english_and_numbers, numbersPlusUnderscore, numbers); + } + + @Test + public void missingDoNotEncodeLettersFromEncodingTest() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Can not use 'do not encode' list because encoding alphabet does not contain"); + + AlphabetConverter.createConverterFromChars(english_and_numbers, lower_case_english, numbers); + } + + @Test + public void missingDoNotEncodeLettersFromOriginalTest() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Can not use 'do not encode' list because original alphabet does not contain"); + + AlphabetConverter.createConverterFromChars(lower_case_english, english_and_numbers, numbers); + } + + private void test(Character[] originalChars, Character[] encodingChars, Character[] doNotEncodeChars, String... strings) throws UnsupportedEncodingException { + + AlphabetConverter ac = AlphabetConverter.createConverterFromChars(originalChars, encodingChars, doNotEncodeChars); + + AlphabetConverter reconstructedAlphabetConverter = AlphabetConverter.createConverterFromMap(ac.getOriginalToEncoded()); + + Assert.assertEquals(ac, reconstructedAlphabetConverter); + Assert.assertEquals(ac.hashCode(), reconstructedAlphabetConverter.hashCode()); + Assert.assertEquals(ac.toString(), reconstructedAlphabetConverter.toString()); + Assert.assertEquals(null, ac.encode(null)); // test null conversions + Assert.assertEquals("", ac.encode("")); // test empty conversion + + // test all the trial strings + for (String s : strings) { + String encoded = ac.encode(s); + + // test that only encoding chars are used + List<Character> originalEncodingChars = Arrays.asList(encodingChars); + for (int i = 0; i < encoded.length(); i++) { + Assert.assertTrue(originalEncodingChars.contains(encoded.charAt(i))); + } + + String decoded = ac.decode(encoded); + + // test that only the original alphabet is used after decoding + List<Character> originalCharsList = Arrays.asList(originalChars); + for (int i = 0; i < decoded.length(); i++) { + Assert.assertTrue(originalCharsList.contains(decoded.charAt(i))); + } + + Assert.assertEquals("Encoded '" + s + "' into '" + encoded + "', but decoded into '" + decoded + "'", s, decoded); + } + } +}