| // © 2016 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html#License |
| /* |
| ******************************************************************************* |
| * Copyright (C) 2001-2013, International Business Machines |
| * Corporation and others. All Rights Reserved. |
| ******************************************************************************* |
| */ |
| |
| package com.ibm.icu.dev.test.bidi; |
| |
| import java.util.Arrays; |
| |
| import com.ibm.icu.dev.test.TestFmwk; |
| import com.ibm.icu.impl.Utility; |
| import com.ibm.icu.lang.UCharacter; |
| import com.ibm.icu.text.Bidi; |
| import com.ibm.icu.text.BidiRun; |
| import com.ibm.icu.util.VersionInfo; |
| |
| /** |
| * A base class for the Bidi test suite. |
| * |
| * @author Lina Kemmel, Matitiahu Allouche |
| */ |
| |
| public class BidiFmwk extends TestFmwk { |
| |
| protected static final char[] charFromDirProp = { |
| /* L R EN ES ET AN CS B S WS ON */ |
| 0x61, 0x5d0, 0x30, 0x2f, 0x25, 0x660, 0x2c, 0xa, 0x9, 0x20, 0x26, |
| /* LRE LRO AL RLE RLO PDF NSM BN */ |
| 0x202a, 0x202d, 0x627, 0x202b, 0x202e, 0x202c, 0x308, 0x200c, |
| /* FSI LRI RLI PDI */ |
| 0x2068, 0x2066, 0x2067, 0x2069 /* new in Unicode 6.3/ICU 52 */ |
| }; |
| |
| static { |
| initCharFromDirProps(); |
| } |
| |
| private static void initCharFromDirProps() { |
| final VersionInfo ucd401 = VersionInfo.getInstance(4, 0, 1, 0); |
| VersionInfo ucdVersion = VersionInfo.getInstance(0, 0, 0, 0); |
| |
| /* lazy initialization */ |
| if (ucdVersion.getMajor() > 0) { |
| return; |
| |
| } |
| ucdVersion = UCharacter.getUnicodeVersion(); |
| if (ucdVersion.compareTo(ucd401) >= 0) { |
| /* Unicode 4.0.1 changes bidi classes for +-/ */ |
| /* change ES character from / to + */ |
| charFromDirProp[TestData.ES] = 0x2b; |
| } |
| } |
| |
| protected boolean assertEquals(String message, String expected, String actual, |
| String src, String mode, String option, |
| String level) { |
| if (expected == null || actual == null) { |
| return super.assertEquals(message, expected, actual); |
| } |
| if (expected.equals(actual)) { |
| return true; |
| } |
| errln(""); |
| errcontln(message); |
| if (src != null) { |
| errcontln("source : \"" + Utility.escape(src) + "\""); |
| } |
| errcontln("expected : \"" + Utility.escape(expected) + "\""); |
| errcontln("actual : \"" + Utility.escape(actual) + "\""); |
| if (mode != null) { |
| errcontln("reordering mode : " + mode); |
| } |
| if (option != null) { |
| errcontln("reordering option : " + option); |
| } |
| if (level != null) { |
| errcontln("paragraph level : " + level); |
| } |
| return false; |
| } |
| |
| protected static String valueOf(int[] array) { |
| StringBuffer result = new StringBuffer(array.length * 4); |
| for (int i = 0; i < array.length; i++) { |
| result.append(' '); |
| result.append(array[i]); |
| } |
| return result.toString(); |
| } |
| |
| private static final String[] modeDescriptions = { |
| "REORDER_DEFAULT", |
| "REORDER_NUMBERS_SPECIAL", |
| "REORDER_GROUP_NUMBERS_WITH_R", |
| "REORDER_RUNS_ONLY", |
| "REORDER_INVERSE_NUMBERS_AS_L", |
| "REORDER_INVERSE_LIKE_DIRECT", |
| "REORDER_INVERSE_FOR_NUMBERS_SPECIAL" |
| }; |
| |
| protected static String modeToString(int mode) { |
| if (mode < Bidi.REORDER_DEFAULT || |
| mode > Bidi.REORDER_INVERSE_FOR_NUMBERS_SPECIAL) { |
| return "INVALID"; |
| } |
| return modeDescriptions[mode]; |
| } |
| |
| private static final short SETPARA_MASK = Bidi.OPTION_INSERT_MARKS | |
| Bidi.OPTION_REMOVE_CONTROLS | Bidi.OPTION_STREAMING; |
| |
| private static final String[] setParaDescriptions = { |
| "OPTION_INSERT_MARKS", |
| "OPTION_REMOVE_CONTROLS", |
| "OPTION_STREAMING" |
| }; |
| |
| protected static String spOptionsToString(int option) { |
| return optionToString(option, SETPARA_MASK, setParaDescriptions); |
| } |
| |
| private static final int MAX_WRITE_REORDERED_OPTION = Bidi.OUTPUT_REVERSE; |
| private static final int REORDER_MASK = (MAX_WRITE_REORDERED_OPTION << 1) - 1; |
| |
| private static final String[] writeReorderedDescriptions = { |
| "KEEP_BASE_COMBINING", // 1 |
| "DO_MIRRORING", // 2 |
| "INSERT_LRM_FOR_NUMERIC", // 4 |
| "REMOVE_BIDI_CONTROLS", // 8 |
| "OUTPUT_REVERSE" // 16 |
| }; |
| |
| public static String wrOptionsToString(int option) { |
| return optionToString(option, REORDER_MASK, writeReorderedDescriptions); |
| } |
| public static String optionToString(int option, int mask, |
| String[] descriptions) { |
| StringBuffer desc = new StringBuffer(50); |
| |
| if ((option &= mask) == 0) { |
| return "0"; |
| } |
| desc.setLength(0); |
| |
| for (int i = 0; option > 0; i++, option >>= 1) { |
| if ((option & 1) != 0) { |
| if (desc.length() > 0) { |
| desc.append(" | "); |
| } |
| desc.append(descriptions[i]); |
| } |
| } |
| return desc.toString(); |
| } |
| |
| static final String columnString = |
| "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; |
| static final char[] columns = columnString.toCharArray(); |
| private static final int TABLE_SIZE = 256; |
| private static boolean tablesInitialized = false; |
| private static char[] pseudoToUChar; |
| private static char[] UCharToPseudo; /* used for Unicode chars < 0x0100 */ |
| private static char[] UCharToPseud2; /* used for Unicode chars >=0x0100 */ |
| |
| static void buildPseudoTables() |
| /* |
| The rules for pseudo-Bidi are as follows: |
| - [ == LRE |
| - ] == RLE |
| - { == LRO |
| - } == RLO |
| - ^ == PDF |
| - @ == LRM |
| - & == RLM |
| - A-F == Arabic Letters 0631-0636 |
| - G-V == Hebrew letters 05d7-05ea |
| - W-Z == Unassigned RTL 08d0-08d3 |
| - 0-5 == western digits 0030-0035 |
| - 6-9 == Arabic-Indic digits 0666-0669 |
| - ` == Combining Grave Accent 0300 (NSM) |
| - ~ == Delete 007f (BN) |
| - | == Paragraph Separator 2029 (B) |
| - _ == Info Separator 1 001f (S) |
| All other characters represent themselves as Latin-1, with the corresponding |
| Bidi properties. |
| */ |
| { |
| int i; |
| char uchar; |
| char c; |
| |
| /* initialize all tables to unknown */ |
| pseudoToUChar = new char[TABLE_SIZE]; |
| UCharToPseudo = new char[TABLE_SIZE]; |
| UCharToPseud2 = new char[TABLE_SIZE]; |
| for (i = 0; i < TABLE_SIZE; i++) { |
| pseudoToUChar[i] = 0xFFFD; |
| UCharToPseudo[i] = '?'; |
| UCharToPseud2[i] = '?'; |
| } |
| /* initialize non letters or digits */ |
| pseudoToUChar[ 0 ] = 0x0000; UCharToPseudo[0x00] = 0 ; |
| pseudoToUChar[' '] = 0x0020; UCharToPseudo[0x20] = ' '; |
| pseudoToUChar['!'] = 0x0021; UCharToPseudo[0x21] = '!'; |
| pseudoToUChar['"'] = 0x0022; UCharToPseudo[0x22] = '"'; |
| pseudoToUChar['#'] = 0x0023; UCharToPseudo[0x23] = '#'; |
| pseudoToUChar['$'] = 0x0024; UCharToPseudo[0x24] = '$'; |
| pseudoToUChar['%'] = 0x0025; UCharToPseudo[0x25] = '%'; |
| pseudoToUChar['\'']= 0x0027; UCharToPseudo[0x27] = '\''; |
| pseudoToUChar['('] = 0x0028; UCharToPseudo[0x28] = '('; |
| pseudoToUChar[')'] = 0x0029; UCharToPseudo[0x29] = ')'; |
| pseudoToUChar['*'] = 0x002A; UCharToPseudo[0x2A] = '*'; |
| pseudoToUChar['+'] = 0x002B; UCharToPseudo[0x2B] = '+'; |
| pseudoToUChar[','] = 0x002C; UCharToPseudo[0x2C] = ','; |
| pseudoToUChar['-'] = 0x002D; UCharToPseudo[0x2D] = '-'; |
| pseudoToUChar['.'] = 0x002E; UCharToPseudo[0x2E] = '.'; |
| pseudoToUChar['/'] = 0x002F; UCharToPseudo[0x2F] = '/'; |
| pseudoToUChar[':'] = 0x003A; UCharToPseudo[0x3A] = ':'; |
| pseudoToUChar[';'] = 0x003B; UCharToPseudo[0x3B] = ';'; |
| pseudoToUChar['<'] = 0x003C; UCharToPseudo[0x3C] = '<'; |
| pseudoToUChar['='] = 0x003D; UCharToPseudo[0x3D] = '='; |
| pseudoToUChar['>'] = 0x003E; UCharToPseudo[0x3E] = '>'; |
| pseudoToUChar['?'] = 0x003F; UCharToPseudo[0x3F] = '?'; |
| pseudoToUChar['\\']= 0x005C; UCharToPseudo[0x5C] = '\\'; |
| /* initialize specially used characters */ |
| pseudoToUChar['`'] = 0x0300; UCharToPseud2[0x00] = '`'; /* NSM */ |
| pseudoToUChar['@'] = 0x200E; UCharToPseud2[0x0E] = '@'; /* LRM */ |
| pseudoToUChar['&'] = 0x200F; UCharToPseud2[0x0F] = '&'; /* RLM */ |
| pseudoToUChar['_'] = 0x001F; UCharToPseudo[0x1F] = '_'; /* S */ |
| pseudoToUChar['|'] = 0x2029; UCharToPseud2[0x29] = '|'; /* B */ |
| pseudoToUChar['['] = 0x202A; UCharToPseud2[0x2A] = '['; /* LRE */ |
| pseudoToUChar[']'] = 0x202B; UCharToPseud2[0x2B] = ']'; /* RLE */ |
| pseudoToUChar['^'] = 0x202C; UCharToPseud2[0x2C] = '^'; /* PDF */ |
| pseudoToUChar['{'] = 0x202D; UCharToPseud2[0x2D] = '{'; /* LRO */ |
| pseudoToUChar['}'] = 0x202E; UCharToPseud2[0x2E] = '}'; /* RLO */ |
| pseudoToUChar['~'] = 0x007F; UCharToPseudo[0x7F] = '~'; /* BN */ |
| /* initialize western digits */ |
| for (i = 0, uchar = 0x0030; i < 6; i++, uchar++) { |
| c = columns[i]; |
| pseudoToUChar[c] = uchar; |
| UCharToPseudo[uchar & 0x00ff] = c; |
| } |
| /* initialize Hindi digits */ |
| for (i = 6, uchar = 0x0666; i < 10; i++, uchar++) { |
| c = columns[i]; |
| pseudoToUChar[c] = uchar; |
| UCharToPseud2[uchar & 0x00ff] = c; |
| } |
| /* initialize Arabic letters */ |
| for (i = 10, uchar = 0x0631; i < 16; i++, uchar++) { |
| c = columns[i]; |
| pseudoToUChar[c] = uchar; |
| UCharToPseud2[uchar & 0x00ff] = c; |
| } |
| /* initialize Hebrew letters */ |
| for (i = 16, uchar = 0x05D7; i < 32; i++, uchar++) { |
| c = columns[i]; |
| pseudoToUChar[c] = uchar; |
| UCharToPseud2[uchar & 0x00ff] = c; |
| } |
| /* initialize Unassigned code points */ |
| for (i = 32, uchar = 0x08D0; i < 36; i++, uchar++) { |
| c = columns[i]; |
| pseudoToUChar[c] = uchar; |
| UCharToPseud2[uchar & 0x00ff] = c; |
| } |
| /* initialize Latin lower case letters */ |
| for (i = 36, uchar = 0x0061; i < 62; i++, uchar++) { |
| c = columns[i]; |
| pseudoToUChar[c] = uchar; |
| UCharToPseudo[uchar & 0x00ff] = c; |
| } |
| tablesInitialized = true; |
| } |
| |
| /*----------------------------------------------------------------------*/ |
| |
| static String pseudoToU16(String input) |
| /* This function converts a pseudo-Bidi string into a char string. |
| It returns the char string. |
| */ |
| { |
| int len = input.length(); |
| char[] output = new char[len]; |
| int i; |
| if (!tablesInitialized) { |
| buildPseudoTables(); |
| } |
| for (i = 0; i < len; i++) |
| output[i] = pseudoToUChar[input.charAt(i)]; |
| return new String(output); |
| } |
| |
| /*----------------------------------------------------------------------*/ |
| |
| static String u16ToPseudo(String input) |
| /* This function converts a char string into a pseudo-Bidi string. |
| It returns the pseudo-Bidi string. |
| */ |
| { |
| int len = input.length(); |
| char[] output = new char[len]; |
| int i; |
| char uchar; |
| if (!tablesInitialized) { |
| buildPseudoTables(); |
| } |
| for (i = 0; i < len; i++) |
| { |
| uchar = input.charAt(i); |
| output[i] = uchar < 0x0100 ? UCharToPseudo[uchar] : |
| UCharToPseud2[uchar & 0x00ff]; |
| } |
| return new String(output); |
| } |
| |
| void errcont(String message) { |
| msg(message, ERR, false, false); |
| } |
| |
| void errcontln(String message) { |
| msg(message, ERR, false, true); |
| } |
| |
| void printCaseInfo(Bidi bidi, String src, String dst) |
| { |
| int length = bidi.getProcessedLength(); |
| byte[] levels = bidi.getLevels(); |
| char[] levelChars = new char[length]; |
| byte lev; |
| int runCount = bidi.countRuns(); |
| errcontln("========================================"); |
| errcontln("Processed length: " + length); |
| for (int i = 0; i < length; i++) { |
| lev = levels[i]; |
| if (lev < 0) { |
| levelChars[i] = '-'; |
| } else if (lev < columns.length) { |
| levelChars[i] = columns[lev]; |
| } else { |
| levelChars[i] = '+'; |
| } |
| } |
| errcontln("Levels: " + new String(levelChars)); |
| errcontln("Source: " + src); |
| errcontln("Result: " + dst); |
| errcontln("Direction: " + bidi.getDirection()); |
| errcontln("paraLevel: " + Byte.toString(bidi.getParaLevel())); |
| errcontln("reorderingMode: " + modeToString(bidi.getReorderingMode())); |
| errcontln("reorderingOptions: " + spOptionsToString(bidi.getReorderingOptions())); |
| errcont("Runs: " + runCount + " => logicalStart.length/level: "); |
| for (int i = 0; i < runCount; i++) { |
| BidiRun run; |
| run = bidi.getVisualRun(i); |
| errcont(" " + run.getStart() + "." + run.getLength() + "/" + |
| run.getEmbeddingLevel()); |
| } |
| errcont("\n"); |
| } |
| |
| static final String mates1 = "<>()[]{}"; |
| static final String mates2 = "><)(][}{"; |
| static final char[] mates1Chars = mates1.toCharArray(); |
| static final char[] mates2Chars = mates2.toCharArray(); |
| |
| boolean matchingPair(Bidi bidi, int i, char c1, char c2) |
| { |
| if (c1 == c2) { |
| return true; |
| } |
| /* For REORDER_RUNS_ONLY, it would not be correct to check levels[i], |
| so we use the appropriate run's level, which is good for all cases. |
| */ |
| if (bidi.getLogicalRun(i).getDirection() == 0) { |
| return false; |
| } |
| for (int k = 0; k < mates1Chars.length; k++) { |
| if ((c1 == mates1Chars[k]) && (c2 == mates2Chars[k])) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| boolean checkWhatYouCan(Bidi bidi, String src, String dst) |
| { |
| int i, idx, logLimit, visLimit; |
| boolean testOK, errMap, errDst; |
| char[] srcChars = src.toCharArray(); |
| char[] dstChars = dst.toCharArray(); |
| int[] visMap = bidi.getVisualMap(); |
| int[] logMap = bidi.getLogicalMap(); |
| |
| testOK = true; |
| errMap = errDst = false; |
| logLimit = bidi.getProcessedLength(); |
| visLimit = bidi.getResultLength(); |
| if (visLimit > dstChars.length) { |
| visLimit = dstChars.length; |
| } |
| char[] accumSrc = new char[logLimit]; |
| char[] accumDst = new char[visLimit]; |
| Arrays.fill(accumSrc, '?'); |
| Arrays.fill(accumDst, '?'); |
| |
| if (logMap.length != logLimit) { |
| errMap = true; |
| } |
| for (i = 0; i < logLimit; i++) { |
| idx = bidi.getVisualIndex(i); |
| if (idx != logMap[i]) { |
| errMap = true; |
| } |
| if (idx == Bidi.MAP_NOWHERE) { |
| continue; |
| } |
| if (idx >= visLimit) { |
| continue; |
| } |
| accumDst[idx] = srcChars[i]; |
| if (!matchingPair(bidi, i, srcChars[i], dstChars[idx])) { |
| errDst = true; |
| } |
| } |
| if (errMap) { |
| if (testOK) { |
| printCaseInfo(bidi, src, dst); |
| testOK = false; |
| } |
| errln("Mismatch between getLogicalMap() and getVisualIndex()"); |
| errcont("Map :" + valueOf(logMap)); |
| errcont("\n"); |
| errcont("Indexes:"); |
| for (i = 0; i < logLimit; i++) { |
| errcont(" " + bidi.getVisualIndex(i)); |
| } |
| errcont("\n"); |
| } |
| if (errDst) { |
| if (testOK) { |
| printCaseInfo(bidi, src, dst); |
| testOK = false; |
| } |
| errln("Source does not map to Result"); |
| errcontln("We got: " + new String(accumDst)); |
| } |
| |
| errMap = errDst = false; |
| if (visMap.length != visLimit) { |
| errMap = true; |
| } |
| for (i = 0; i < visLimit; i++) { |
| idx = bidi.getLogicalIndex(i); |
| if (idx != visMap[i]) { |
| errMap = true; |
| } |
| if (idx == Bidi.MAP_NOWHERE) { |
| continue; |
| } |
| if (idx >= logLimit) { |
| continue; |
| } |
| accumSrc[idx] = dstChars[i]; |
| if (!matchingPair(bidi, idx, srcChars[idx], dstChars[i])) { |
| errDst = true; |
| } |
| } |
| if (errMap) { |
| if (testOK) { |
| printCaseInfo(bidi, src, dst); |
| testOK = false; |
| } |
| errln("Mismatch between getVisualMap() and getLogicalIndex()"); |
| errcont("Map :" + valueOf(visMap)); |
| errcont("\n"); |
| errcont("Indexes:"); |
| for (i = 0; i < visLimit; i++) { |
| errcont(" " + bidi.getLogicalIndex(i)); |
| } |
| errcont("\n"); |
| } |
| if (errDst) { |
| if (testOK) { |
| printCaseInfo(bidi, src, dst); |
| testOK = false; |
| } |
| errln("Result does not map to Source"); |
| errcontln("We got: " + new String(accumSrc)); |
| } |
| return testOK; |
| } |
| |
| } |