1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // © 2017 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 package ohos.global.icu.impl.locale; 5 6 import java.nio.ByteBuffer; 7 import java.util.Arrays; 8 import java.util.Collections; 9 import java.util.LinkedHashSet; 10 import java.util.Map; 11 import java.util.MissingResourceException; 12 import java.util.Set; 13 import java.util.TreeMap; 14 15 import ohos.global.icu.impl.ICUData; 16 import ohos.global.icu.impl.ICUResourceBundle; 17 import ohos.global.icu.impl.UResource; 18 import ohos.global.icu.util.BytesTrie; 19 import ohos.global.icu.util.LocaleMatcher; 20 import ohos.global.icu.util.LocaleMatcher.FavorSubtag; 21 import ohos.global.icu.util.ULocale; 22 23 /** 24 * Offline-built data for LocaleMatcher. 25 * Mostly but not only the data for mapping locales to their maximized forms. 26 * @hide exposed on OHOS 27 */ 28 public class LocaleDistance { 29 /** 30 * Bit flag used on the last character of a subtag in the trie. 31 * Must be set consistently by the builder and the lookup code. 32 */ 33 public static final int END_OF_SUBTAG = 0x80; 34 /** Distance value bit flag, set by the builder. */ 35 public static final int DISTANCE_SKIP_SCRIPT = 0x80; 36 /** Distance value bit flag, set by trieNext(). */ 37 private static final int DISTANCE_IS_FINAL = 0x100; 38 private static final int DISTANCE_IS_FINAL_OR_SKIP_SCRIPT = 39 DISTANCE_IS_FINAL | DISTANCE_SKIP_SCRIPT; 40 41 // The distance is shifted left to gain some fraction bits. 42 private static final int DISTANCE_SHIFT = 3; 43 private static final int DISTANCE_FRACTION_MASK = 7; 44 // 7 bits for 0..100 45 private static final int DISTANCE_INT_SHIFT = 7; 46 private static final int INDEX_SHIFT = DISTANCE_INT_SHIFT + DISTANCE_SHIFT; 47 private static final int DISTANCE_MASK = 0x3ff; 48 // vate static final int MAX_INDEX = 0x1fffff; // avoids sign bit 49 private static final int INDEX_NEG_1 = 0xfffffc00; 50 51 // Indexes into array of distances. 52 public static final int IX_DEF_LANG_DISTANCE = 0; 53 public static final int IX_DEF_SCRIPT_DISTANCE = 1; 54 public static final int IX_DEF_REGION_DISTANCE = 2; 55 public static final int IX_MIN_REGION_DISTANCE = 3; 56 public static final int IX_LIMIT = 4; 57 private static final int ABOVE_THRESHOLD = 100; 58 59 private static final boolean DEBUG_OUTPUT = LSR.DEBUG_OUTPUT; 60 61 // The trie maps each dlang+slang+dscript+sscript+dregion+sregion 62 // (encoded in ASCII with bit 7 set on the last character of each subtag) to a distance. 63 // There is also a trie value for each subsequence of whole subtags. 64 // One '*' is used for a (desired, supported) pair of "und", "Zzzz"/"", or "ZZ"/"". 65 private final BytesTrie trie; 66 67 /** 68 * Maps each region to zero or more single-character partitions. 69 */ 70 private final byte[] regionToPartitionsIndex; 71 private final String[] partitionArrays; 72 73 /** 74 * Used to get the paradigm region for a cluster, if there is one. 75 */ 76 private final Set<LSR> paradigmLSRs; 77 78 private final int defaultLanguageDistance; 79 private final int defaultScriptDistance; 80 private final int defaultRegionDistance; 81 private final int minRegionDistance; 82 private final int defaultDemotionPerDesiredLocale; 83 shiftDistance(int distance)84 public static final int shiftDistance(int distance) { 85 return distance << DISTANCE_SHIFT; 86 } 87 getShiftedDistance(int indexAndDistance)88 public static final int getShiftedDistance(int indexAndDistance) { 89 return indexAndDistance & DISTANCE_MASK; 90 } 91 getDistanceDouble(int indexAndDistance)92 public static final double getDistanceDouble(int indexAndDistance) { 93 double shiftedDistance = getShiftedDistance(indexAndDistance); 94 return shiftedDistance / (1 << DISTANCE_SHIFT); 95 } 96 getDistanceFloor(int indexAndDistance)97 private static final int getDistanceFloor(int indexAndDistance) { 98 return (indexAndDistance & DISTANCE_MASK) >> DISTANCE_SHIFT; 99 } 100 getIndex(int indexAndDistance)101 public static final int getIndex(int indexAndDistance) { 102 assert indexAndDistance >= 0; 103 return indexAndDistance >> INDEX_SHIFT; 104 } 105 106 // VisibleForTesting 107 /** 108 * @hide exposed on OHOS 109 */ 110 public static final class Data { 111 public byte[] trie; 112 public byte[] regionToPartitionsIndex; 113 public String[] partitionArrays; 114 public Set<LSR> paradigmLSRs; 115 public int[] distances; 116 Data(byte[] trie, byte[] regionToPartitionsIndex, String[] partitionArrays, Set<LSR> paradigmLSRs, int[] distances)117 public Data(byte[] trie, 118 byte[] regionToPartitionsIndex, String[] partitionArrays, 119 Set<LSR> paradigmLSRs, int[] distances) { 120 this.trie = trie; 121 this.regionToPartitionsIndex = regionToPartitionsIndex; 122 this.partitionArrays = partitionArrays; 123 this.paradigmLSRs = paradigmLSRs; 124 this.distances = distances; 125 } 126 getValue(UResource.Table table, String key, UResource.Value value)127 private static UResource.Value getValue(UResource.Table table, 128 String key, UResource.Value value) { 129 if (!table.findValue(key, value)) { 130 throw new MissingResourceException( 131 "langInfo.res missing data", "", "match/" + key); 132 } 133 return value; 134 } 135 136 // VisibleForTesting load()137 public static Data load() throws MissingResourceException { 138 ICUResourceBundle langInfo = ICUResourceBundle.getBundleInstance( 139 ICUData.ICU_BASE_NAME, "langInfo", 140 ICUResourceBundle.ICU_DATA_CLASS_LOADER, ICUResourceBundle.OpenType.DIRECT); 141 UResource.Value value = langInfo.getValueWithFallback("match"); 142 UResource.Table matchTable = value.getTable(); 143 144 ByteBuffer buffer = getValue(matchTable, "trie", value).getBinary(); 145 byte[] trie = new byte[buffer.remaining()]; 146 buffer.get(trie); 147 148 buffer = getValue(matchTable, "regionToPartitions", value).getBinary(); 149 byte[] regionToPartitions = new byte[buffer.remaining()]; 150 buffer.get(regionToPartitions); 151 if (regionToPartitions.length < LSR.REGION_INDEX_LIMIT) { 152 throw new MissingResourceException( 153 "langInfo.res binary data too short", "", "match/regionToPartitions"); 154 } 155 156 String[] partitions = getValue(matchTable, "partitions", value).getStringArray(); 157 158 Set<LSR> paradigmLSRs; 159 if (matchTable.findValue("paradigms", value)) { 160 String[] paradigms = value.getStringArray(); 161 // LinkedHashSet for stable order; otherwise a unit test is flaky. 162 paradigmLSRs = new LinkedHashSet<>(paradigms.length / 3); 163 for (int i = 0; i < paradigms.length; i += 3) { 164 paradigmLSRs.add(new LSR(paradigms[i], paradigms[i + 1], paradigms[i + 2], 165 LSR.DONT_CARE_FLAGS)); 166 } 167 } else { 168 paradigmLSRs = Collections.emptySet(); 169 } 170 171 int[] distances = getValue(matchTable, "distances", value).getIntVector(); 172 if (distances.length < IX_LIMIT) { 173 throw new MissingResourceException( 174 "langInfo.res intvector too short", "", "match/distances"); 175 } 176 177 return new Data(trie, regionToPartitions, partitions, paradigmLSRs, distances); 178 } 179 180 @Override equals(Object other)181 public boolean equals(Object other) { 182 if (this == other) { return true; } 183 if (other == null || !getClass().equals(other.getClass())) { return false; } 184 Data od = (Data)other; 185 return Arrays.equals(trie, od.trie) && 186 Arrays.equals(regionToPartitionsIndex, od.regionToPartitionsIndex) && 187 Arrays.equals(partitionArrays, od.partitionArrays) && 188 paradigmLSRs.equals(od.paradigmLSRs) && 189 Arrays.equals(distances, od.distances); 190 } 191 192 @Override hashCode()193 public int hashCode() { // unused; silence ErrorProne 194 return 1; 195 } 196 } 197 198 // VisibleForTesting 199 public static final LocaleDistance INSTANCE = new LocaleDistance(Data.load()); 200 LocaleDistance(Data data)201 private LocaleDistance(Data data) { 202 trie = new BytesTrie(data.trie, 0); 203 regionToPartitionsIndex = data.regionToPartitionsIndex; 204 partitionArrays = data.partitionArrays; 205 paradigmLSRs = data.paradigmLSRs; 206 defaultLanguageDistance = data.distances[IX_DEF_LANG_DISTANCE]; 207 defaultScriptDistance = data.distances[IX_DEF_SCRIPT_DISTANCE]; 208 defaultRegionDistance = data.distances[IX_DEF_REGION_DISTANCE]; 209 minRegionDistance = data.distances[IX_MIN_REGION_DISTANCE]; 210 211 // For the default demotion value, use the 212 // default region distance between unrelated Englishes. 213 // Thus, unless demotion is turned off, 214 // a mere region difference for one desired locale 215 // is as good as a perfect match for the next following desired locale. 216 // As of CLDR 36, we have <languageMatch desired="en_*_*" supported="en_*_*" distance="5"/>. 217 LSR en = new LSR("en", "Latn", "US", LSR.EXPLICIT_LSR); 218 LSR enGB = new LSR("en", "Latn", "GB", LSR.EXPLICIT_LSR); 219 int indexAndDistance = getBestIndexAndDistance(en, new LSR[] { enGB }, 1, 220 shiftDistance(50), FavorSubtag.LANGUAGE, LocaleMatcher.Direction.WITH_ONE_WAY); 221 defaultDemotionPerDesiredLocale = getDistanceFloor(indexAndDistance); 222 223 if (DEBUG_OUTPUT) { 224 System.out.println("*** locale distance"); 225 System.out.println("defaultLanguageDistance=" + defaultLanguageDistance); 226 System.out.println("defaultScriptDistance=" + defaultScriptDistance); 227 System.out.println("defaultRegionDistance=" + defaultRegionDistance); 228 testOnlyPrintDistanceTable(); 229 } 230 } 231 232 // VisibleForTesting testOnlyDistance(ULocale desired, ULocale supported, int threshold, FavorSubtag favorSubtag)233 public int testOnlyDistance(ULocale desired, ULocale supported, 234 int threshold, FavorSubtag favorSubtag) { 235 LSR supportedLSR = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(supported); 236 LSR desiredLSR = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(desired); 237 int indexAndDistance = getBestIndexAndDistance(desiredLSR, new LSR[] { supportedLSR }, 1, 238 shiftDistance(threshold), favorSubtag, LocaleMatcher.Direction.WITH_ONE_WAY); 239 return getDistanceFloor(indexAndDistance); 240 } 241 242 /** 243 * Finds the supported LSR with the smallest distance from the desired one. 244 * Equivalent LSR subtags must be normalized into a canonical form. 245 * 246 * <p>Returns the index of the lowest-distance supported LSR in the high bits 247 * (negative if none has a distance below the threshold), 248 * and its distance (0..ABOVE_THRESHOLD) in the low bits. 249 */ getBestIndexAndDistance(LSR desired, LSR[] supportedLSRs, int supportedLSRsLength, int shiftedThreshold, FavorSubtag favorSubtag, LocaleMatcher.Direction direction)250 public int getBestIndexAndDistance(LSR desired, LSR[] supportedLSRs, int supportedLSRsLength, 251 int shiftedThreshold, FavorSubtag favorSubtag, LocaleMatcher.Direction direction) { 252 BytesTrie iter = new BytesTrie(trie); 253 // Look up the desired language only once for all supported LSRs. 254 // Its "distance" is either a match point value of 0, or a non-match negative value. 255 // Note: The data builder verifies that there are no <*, supported> or <desired, *> rules. 256 int desLangDistance = trieNext(iter, desired.language, false); 257 long desLangState = desLangDistance >= 0 && supportedLSRsLength > 1 ? iter.getState64() : 0; 258 // Index of the supported LSR with the lowest distance. 259 int bestIndex = -1; 260 // Cached lookup info from XLikelySubtags.compareLikely(). 261 int bestLikelyInfo = -1; 262 for (int slIndex = 0; slIndex < supportedLSRsLength; ++slIndex) { 263 LSR supported = supportedLSRs[slIndex]; 264 boolean star = false; 265 int distance = desLangDistance; 266 if (distance >= 0) { 267 assert (distance & DISTANCE_IS_FINAL) == 0; 268 if (slIndex != 0) { 269 iter.resetToState64(desLangState); 270 } 271 distance = trieNext(iter, supported.language, true); 272 } 273 // Note: The data builder verifies that there are no rules with "any" (*) language and 274 // real (non *) script or region subtags. 275 // This means that if the lookup for either language fails we can use 276 // the default distances without further lookups. 277 int flags; 278 if (distance >= 0) { 279 flags = distance & DISTANCE_IS_FINAL_OR_SKIP_SCRIPT; 280 distance &= ~DISTANCE_IS_FINAL_OR_SKIP_SCRIPT; 281 } else { // <*, *> 282 if (desired.language.equals(supported.language)) { 283 distance = 0; 284 } else { 285 distance = defaultLanguageDistance; 286 } 287 flags = 0; 288 star = true; 289 } 290 assert 0 <= distance && distance <= 100; 291 // Round up the shifted threshold (if fraction bits are not 0) 292 // for comparison with un-shifted distances until we need fraction bits. 293 // (If we simply shifted non-zero fraction bits away, then we might ignore a language 294 // when it's really still a micro distance below the threshold.) 295 int roundedThreshold = (shiftedThreshold + DISTANCE_FRACTION_MASK) >> DISTANCE_SHIFT; 296 // We implement "favor subtag" by reducing the language subtag distance 297 // (unscientifically reducing it to a quarter of the normal value), 298 // so that the script distance is relatively more important. 299 // For example, given a default language distance of 80, we reduce it to 20, 300 // which is below the default threshold of 50, which is the default script distance. 301 if (favorSubtag == FavorSubtag.SCRIPT) { 302 distance >>= 2; 303 } 304 // Let distance == roundedThreshold pass until the tie-breaker logic 305 // at the end of the loop. 306 if (distance > roundedThreshold) { 307 continue; 308 } 309 310 int scriptDistance; 311 if (star || flags != 0) { 312 if (desired.script.equals(supported.script)) { 313 scriptDistance = 0; 314 } else { 315 scriptDistance = defaultScriptDistance; 316 } 317 } else { 318 scriptDistance = getDesSuppScriptDistance(iter, iter.getState64(), 319 desired.script, supported.script); 320 flags = scriptDistance & DISTANCE_IS_FINAL; 321 scriptDistance &= ~DISTANCE_IS_FINAL; 322 } 323 distance += scriptDistance; 324 if (distance > roundedThreshold) { 325 continue; 326 } 327 328 if (desired.region.equals(supported.region)) { 329 // regionDistance = 0 330 } else if (star || (flags & DISTANCE_IS_FINAL) != 0) { 331 distance += defaultRegionDistance; 332 } else { 333 int remainingThreshold = roundedThreshold - distance; 334 if (minRegionDistance > remainingThreshold) { 335 continue; 336 } 337 338 // From here on we know the regions are not equal. 339 // Map each region to zero or more partitions. (zero = one non-matching string) 340 // (Each array of single-character partition strings is encoded as one string.) 341 // If either side has more than one, then we find the maximum distance. 342 // This could be optimized by adding some more structure, but probably not worth it. 343 distance += getRegionPartitionsDistance( 344 iter, iter.getState64(), 345 partitionsForRegion(desired), 346 partitionsForRegion(supported), 347 remainingThreshold); 348 } 349 int shiftedDistance = shiftDistance(distance); 350 if (shiftedDistance == 0) { 351 // Distinguish between equivalent but originally unequal locales via an 352 // additional micro distance. 353 shiftedDistance |= (desired.flags ^ supported.flags); 354 if (shiftedDistance < shiftedThreshold) { 355 if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY || 356 // Is there also a match when we swap desired/supported? 357 isMatch(supported, desired, shiftedThreshold, favorSubtag)) { 358 if (shiftedDistance == 0) { 359 return slIndex << INDEX_SHIFT; 360 } 361 bestIndex = slIndex; 362 shiftedThreshold = shiftedDistance; 363 bestLikelyInfo = -1; 364 } 365 } 366 } else { 367 if (shiftedDistance < shiftedThreshold) { 368 if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY || 369 // Is there also a match when we swap desired/supported? 370 isMatch(supported, desired, shiftedThreshold, favorSubtag)) { 371 bestIndex = slIndex; 372 shiftedThreshold = shiftedDistance; 373 bestLikelyInfo = -1; 374 } 375 } else if (shiftedDistance == shiftedThreshold && bestIndex >= 0) { 376 if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY || 377 // Is there also a match when we swap desired/supported? 378 isMatch(supported, desired, shiftedThreshold, favorSubtag)) { 379 bestLikelyInfo = XLikelySubtags.INSTANCE.compareLikely( 380 supported, supportedLSRs[bestIndex], bestLikelyInfo); 381 if ((bestLikelyInfo & 1) != 0) { 382 // This supported locale matches as well as the previous best match, 383 // and neither matches perfectly, 384 // but this one is "more likely" (has more-default subtags). 385 bestIndex = slIndex; 386 } 387 } 388 } 389 } 390 } 391 return bestIndex >= 0 ? 392 (bestIndex << INDEX_SHIFT) | shiftedThreshold : 393 INDEX_NEG_1 | shiftDistance(ABOVE_THRESHOLD); 394 } 395 isMatch(LSR desired, LSR supported, int shiftedThreshold, FavorSubtag favorSubtag)396 private boolean isMatch(LSR desired, LSR supported, 397 int shiftedThreshold, FavorSubtag favorSubtag) { 398 return getBestIndexAndDistance( 399 desired, new LSR[] { supported }, 1, 400 shiftedThreshold, favorSubtag, null) >= 0; 401 } 402 getDesSuppScriptDistance(BytesTrie iter, long startState, String desired, String supported)403 private static final int getDesSuppScriptDistance(BytesTrie iter, long startState, 404 String desired, String supported) { 405 // Note: The data builder verifies that there are no <*, supported> or <desired, *> rules. 406 int distance = trieNext(iter, desired, false); 407 if (distance >= 0) { 408 distance = trieNext(iter, supported, true); 409 } 410 if (distance < 0) { 411 BytesTrie.Result result = iter.resetToState64(startState).next('*'); // <*, *> 412 assert result.hasValue(); 413 if (desired.equals(supported)) { 414 distance = 0; // same script 415 } else { 416 distance = iter.getValue(); 417 assert distance >= 0; 418 } 419 if (result == BytesTrie.Result.FINAL_VALUE) { 420 distance |= DISTANCE_IS_FINAL; 421 } 422 } 423 return distance; 424 } 425 getRegionPartitionsDistance(BytesTrie iter, long startState, String desiredPartitions, String supportedPartitions, int threshold)426 private static final int getRegionPartitionsDistance(BytesTrie iter, long startState, 427 String desiredPartitions, String supportedPartitions, int threshold) { 428 int desLength = desiredPartitions.length(); 429 int suppLength = supportedPartitions.length(); 430 if (desLength == 1 && suppLength == 1) { 431 // Fastpath for single desired/supported partitions. 432 BytesTrie.Result result = iter.next(desiredPartitions.charAt(0) | END_OF_SUBTAG); 433 if (result.hasNext()) { 434 result = iter.next(supportedPartitions.charAt(0) | END_OF_SUBTAG); 435 if (result.hasValue()) { 436 return iter.getValue(); 437 } 438 } 439 return getFallbackRegionDistance(iter, startState); 440 } 441 442 int regionDistance = 0; 443 // Fall back to * only once, not for each pair of partition strings. 444 boolean star = false; 445 for (int di = 0;;) { 446 // Look up each desired-partition string only once, 447 // not for each (desired, supported) pair. 448 BytesTrie.Result result = iter.next(desiredPartitions.charAt(di++) | END_OF_SUBTAG); 449 if (result.hasNext()) { 450 long desState = suppLength > 1 ? iter.getState64() : 0; 451 for (int si = 0;;) { 452 result = iter.next(supportedPartitions.charAt(si++) | END_OF_SUBTAG); 453 int d; 454 if (result.hasValue()) { 455 d = iter.getValue(); 456 } else if (star) { 457 d = 0; 458 } else { 459 d = getFallbackRegionDistance(iter, startState); 460 star = true; 461 } 462 if (d > threshold) { 463 return d; 464 } else if (regionDistance < d) { 465 regionDistance = d; 466 } 467 if (si < suppLength) { 468 iter.resetToState64(desState); 469 } else { 470 break; 471 } 472 } 473 } else if (!star) { 474 int d = getFallbackRegionDistance(iter, startState); 475 if (d > threshold) { 476 return d; 477 } else if (regionDistance < d) { 478 regionDistance = d; 479 } 480 star = true; 481 } 482 if (di < desLength) { 483 iter.resetToState64(startState); 484 } else { 485 break; 486 } 487 } 488 return regionDistance; 489 } 490 getFallbackRegionDistance(BytesTrie iter, long startState)491 private static final int getFallbackRegionDistance(BytesTrie iter, long startState) { 492 BytesTrie.Result result = iter.resetToState64(startState).next('*'); // <*, *> 493 assert result.hasValue(); 494 int distance = iter.getValue(); 495 assert distance >= 0; 496 return distance; 497 } 498 trieNext(BytesTrie iter, String s, boolean wantValue)499 private static final int trieNext(BytesTrie iter, String s, boolean wantValue) { 500 if (s.isEmpty()) { 501 return -1; // no empty subtags in the distance data 502 } 503 for (int i = 0, end = s.length() - 1;; ++i) { 504 int c = s.charAt(i); 505 if (i < end) { 506 if (!iter.next(c).hasNext()) { 507 return -1; 508 } 509 } else { 510 // last character of this subtag 511 BytesTrie.Result result = iter.next(c | END_OF_SUBTAG); 512 if (wantValue) { 513 if (result.hasValue()) { 514 int value = iter.getValue(); 515 if (result == BytesTrie.Result.FINAL_VALUE) { 516 value |= DISTANCE_IS_FINAL; 517 } 518 return value; 519 } 520 } else { 521 if (result.hasNext()) { 522 return 0; 523 } 524 } 525 return -1; 526 } 527 } 528 } 529 530 @Override toString()531 public String toString() { 532 return testOnlyGetDistanceTable().toString(); 533 } 534 partitionsForRegion(LSR lsr)535 private String partitionsForRegion(LSR lsr) { 536 // ill-formed region -> one non-matching string 537 int pIndex = regionToPartitionsIndex[lsr.regionIndex]; 538 return partitionArrays[pIndex]; 539 } 540 isParadigmLSR(LSR lsr)541 public boolean isParadigmLSR(LSR lsr) { 542 // Linear search for a very short list (length 6 as of 2019), 543 // because we look for equivalence not equality, and 544 // HashSet does not support customizing equality. 545 // If there are many paradigm LSRs we should revisit this. 546 assert paradigmLSRs.size() <= 15; 547 for (LSR plsr : paradigmLSRs) { 548 if (lsr.isEquivalentTo(plsr)) { 549 return true; 550 } 551 } 552 return false; 553 } 554 555 // VisibleForTesting getDefaultScriptDistance()556 public int getDefaultScriptDistance() { 557 return defaultScriptDistance; 558 } 559 getDefaultRegionDistance()560 int getDefaultRegionDistance() { 561 return defaultRegionDistance; 562 } 563 getDefaultDemotionPerDesiredLocale()564 public int getDefaultDemotionPerDesiredLocale() { 565 return defaultDemotionPerDesiredLocale; 566 } 567 568 // VisibleForTesting testOnlyGetDistanceTable()569 public Map<String, Integer> testOnlyGetDistanceTable() { 570 Map<String, Integer> map = new TreeMap<>(); 571 StringBuilder sb = new StringBuilder(); 572 for (BytesTrie.Entry entry : trie) { 573 sb.setLength(0); 574 int length = entry.bytesLength(); 575 for (int i = 0; i < length; ++i) { 576 byte b = entry.byteAt(i); 577 if (b == '*') { 578 // One * represents a (desired, supported) = (ANY, ANY) pair. 579 sb.append("*-*-"); 580 } else { 581 if (b >= 0) { 582 sb.append((char) b); 583 } else { // end of subtag 584 sb.append((char) (b & 0x7f)).append('-'); 585 } 586 } 587 } 588 assert sb.length() > 0 && sb.charAt(sb.length() - 1) == '-'; 589 sb.setLength(sb.length() - 1); 590 map.put(sb.toString(), entry.value); 591 } 592 return map; 593 } 594 595 // VisibleForTesting testOnlyPrintDistanceTable()596 public void testOnlyPrintDistanceTable() { 597 for (Map.Entry<String, Integer> mapping : testOnlyGetDistanceTable().entrySet()) { 598 String suffix = ""; 599 int value = mapping.getValue(); 600 if ((value & DISTANCE_SKIP_SCRIPT) != 0) { 601 value &= ~DISTANCE_SKIP_SCRIPT; 602 suffix = " skip script"; 603 } 604 System.out.println(mapping.getKey() + '=' + value + suffix); 605 } 606 } 607 } 608