1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 package com.android.providers.contacts; 17 18 import android.content.ContentValues; 19 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 20 import android.provider.ContactsContract.FullNameStyle; 21 import android.provider.ContactsContract.PhoneticNameStyle; 22 import android.text.TextUtils; 23 import android.util.ArraySet; 24 25 import com.android.providers.contacts.util.NeededForTesting; 26 27 import java.lang.Character.UnicodeBlock; 28 import java.util.Locale; 29 import java.util.StringTokenizer; 30 31 /** 32 * The purpose of this class is to split a full name into given names and last 33 * name. The logic only supports having a single last name. If the full name has 34 * multiple last names the output will be incorrect. 35 * <p> 36 * Core algorithm: 37 * <ol> 38 * <li>Remove the suffixes (III, Ph.D., M.D.).</li> 39 * <li>Remove the prefixes (Mr., Pastor, Reverend, Sir).</li> 40 * <li>Assign the last remaining token as the last name.</li> 41 * <li>If the previous word to the last name is one from LASTNAME_PREFIXES, use 42 * this word also as the last name.</li> 43 * <li>Assign the rest of the words as the "given names".</li> 44 * </ol> 45 */ 46 public class NameSplitter { 47 48 public static final int MAX_TOKENS = 10; 49 50 private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase(); 51 private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase(); 52 53 // This includes simplified and traditional Chinese 54 private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase(); 55 56 private final ArraySet<String> mPrefixesSet; 57 private final ArraySet<String> mSuffixesSet; 58 private final int mMaxSuffixLength; 59 private final ArraySet<String> mLastNamePrefixesSet; 60 private final ArraySet<String> mConjuctions; 61 private final Locale mLocale; 62 private final String mLanguage; 63 64 /** 65 * Two-Chracter long Korean family names. 66 * http://ko.wikipedia.org/wiki/%ED%95%9C%EA%B5%AD%EC%9D%98_%EB%B3%B5%EC%84%B1 67 */ 68 private static final String[] KOREAN_TWO_CHARCTER_FAMILY_NAMES = { 69 "\uAC15\uC804", // Gang Jeon 70 "\uB0A8\uAD81", // Nam Goong 71 "\uB3C5\uACE0", // Dok Go 72 "\uB3D9\uBC29", // Dong Bang 73 "\uB9DD\uC808", // Mang Jeol 74 "\uC0AC\uACF5", // Sa Gong 75 "\uC11C\uBB38", // Seo Moon 76 "\uC120\uC6B0", // Seon Woo 77 "\uC18C\uBD09", // So Bong 78 "\uC5B4\uAE08", // Uh Geum 79 "\uC7A5\uACE1", // Jang Gok 80 "\uC81C\uAC08", // Je Gal 81 "\uD669\uBCF4" // Hwang Bo 82 }; 83 84 public static class Name { 85 public String prefix; 86 public String givenNames; 87 public String middleName; 88 public String familyName; 89 public String suffix; 90 91 public int fullNameStyle; 92 93 public String phoneticFamilyName; 94 public String phoneticMiddleName; 95 public String phoneticGivenName; 96 97 public int phoneticNameStyle; 98 Name()99 public Name() { 100 } 101 Name(String prefix, String givenNames, String middleName, String familyName, String suffix)102 public Name(String prefix, String givenNames, String middleName, String familyName, 103 String suffix) { 104 this.prefix = prefix; 105 this.givenNames = givenNames; 106 this.middleName = middleName; 107 this.familyName = familyName; 108 this.suffix = suffix; 109 } 110 111 @NeededForTesting getPrefix()112 public String getPrefix() { 113 return prefix; 114 } 115 getGivenNames()116 public String getGivenNames() { 117 return givenNames; 118 } 119 getMiddleName()120 public String getMiddleName() { 121 return middleName; 122 } 123 getFamilyName()124 public String getFamilyName() { 125 return familyName; 126 } 127 128 @NeededForTesting getSuffix()129 public String getSuffix() { 130 return suffix; 131 } 132 getFullNameStyle()133 public int getFullNameStyle() { 134 return fullNameStyle; 135 } 136 getPhoneticFamilyName()137 public String getPhoneticFamilyName() { 138 return phoneticFamilyName; 139 } 140 getPhoneticMiddleName()141 public String getPhoneticMiddleName() { 142 return phoneticMiddleName; 143 } 144 getPhoneticGivenName()145 public String getPhoneticGivenName() { 146 return phoneticGivenName; 147 } 148 getPhoneticNameStyle()149 public int getPhoneticNameStyle() { 150 return phoneticNameStyle; 151 } 152 fromValues(ContentValues values)153 public void fromValues(ContentValues values) { 154 prefix = values.getAsString(StructuredName.PREFIX); 155 givenNames = values.getAsString(StructuredName.GIVEN_NAME); 156 middleName = values.getAsString(StructuredName.MIDDLE_NAME); 157 familyName = values.getAsString(StructuredName.FAMILY_NAME); 158 suffix = values.getAsString(StructuredName.SUFFIX); 159 160 Integer integer = values.getAsInteger(StructuredName.FULL_NAME_STYLE); 161 fullNameStyle = integer == null ? FullNameStyle.UNDEFINED : integer; 162 163 phoneticFamilyName = values.getAsString(StructuredName.PHONETIC_FAMILY_NAME); 164 phoneticMiddleName = values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); 165 phoneticGivenName = values.getAsString(StructuredName.PHONETIC_GIVEN_NAME); 166 167 integer = values.getAsInteger(StructuredName.PHONETIC_NAME_STYLE); 168 phoneticNameStyle = integer == null ? PhoneticNameStyle.UNDEFINED : integer; 169 } 170 toValues(ContentValues values)171 public void toValues(ContentValues values) { 172 putValueIfPresent(values, StructuredName.PREFIX, prefix); 173 putValueIfPresent(values, StructuredName.GIVEN_NAME, givenNames); 174 putValueIfPresent(values, StructuredName.MIDDLE_NAME, middleName); 175 putValueIfPresent(values, StructuredName.FAMILY_NAME, familyName); 176 putValueIfPresent(values, StructuredName.SUFFIX, suffix); 177 values.put(StructuredName.FULL_NAME_STYLE, fullNameStyle); 178 putValueIfPresent(values, StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName); 179 putValueIfPresent(values, StructuredName.PHONETIC_MIDDLE_NAME, phoneticMiddleName); 180 putValueIfPresent(values, StructuredName.PHONETIC_GIVEN_NAME, phoneticGivenName); 181 values.put(StructuredName.PHONETIC_NAME_STYLE, phoneticNameStyle); 182 } 183 putValueIfPresent(ContentValues values, String name, String value)184 private void putValueIfPresent(ContentValues values, String name, String value) { 185 if (value != null) { 186 values.put(name, value); 187 } 188 } 189 clear()190 public void clear() { 191 prefix = null; 192 givenNames = null; 193 middleName = null; 194 familyName = null; 195 suffix = null; 196 fullNameStyle = FullNameStyle.UNDEFINED; 197 phoneticFamilyName = null; 198 phoneticMiddleName = null; 199 phoneticGivenName = null; 200 phoneticNameStyle = PhoneticNameStyle.UNDEFINED; 201 } 202 isEmpty()203 public boolean isEmpty() { 204 return TextUtils.isEmpty(givenNames) 205 && TextUtils.isEmpty(middleName) 206 && TextUtils.isEmpty(familyName) 207 && TextUtils.isEmpty(suffix) 208 && TextUtils.isEmpty(phoneticFamilyName) 209 && TextUtils.isEmpty(phoneticMiddleName) 210 && TextUtils.isEmpty(phoneticGivenName); 211 } 212 213 @Override toString()214 public String toString() { 215 return "[prefix: " + prefix + " given: " + givenNames + " middle: " + middleName 216 + " family: " + familyName + " suffix: " + suffix + " ph/given: " 217 + phoneticGivenName + " ph/middle: " + phoneticMiddleName + " ph/family: " 218 + phoneticFamilyName + "]"; 219 } 220 } 221 222 private static class NameTokenizer extends StringTokenizer { 223 private final String[] mTokens; 224 private int mDotBitmask; 225 private int mCommaBitmask; 226 private int mStartPointer; 227 private int mEndPointer; 228 NameTokenizer(String fullName)229 public NameTokenizer(String fullName) { 230 super(fullName, " .,", true); 231 232 mTokens = new String[MAX_TOKENS]; 233 234 // Iterate over tokens, skipping over empty ones and marking tokens that 235 // are followed by dots. 236 while (hasMoreTokens() && mEndPointer < MAX_TOKENS) { 237 final String token = nextToken(); 238 if (token.length() > 0) { 239 final char c = token.charAt(0); 240 if (c == ' ') { 241 continue; 242 } 243 } 244 245 if (mEndPointer > 0 && token.charAt(0) == '.') { 246 mDotBitmask |= (1 << (mEndPointer - 1)); 247 } else if (mEndPointer > 0 && token.charAt(0) == ',') { 248 mCommaBitmask |= (1 << (mEndPointer - 1)); 249 } else { 250 mTokens[mEndPointer] = token; 251 mEndPointer++; 252 } 253 } 254 } 255 256 /** 257 * Returns true if the token is followed by a dot in the original full name. 258 */ hasDot(int index)259 public boolean hasDot(int index) { 260 return (mDotBitmask & (1 << index)) != 0; 261 } 262 263 /** 264 * Returns true if the token is followed by a comma in the original full name. 265 */ hasComma(int index)266 public boolean hasComma(int index) { 267 return (mCommaBitmask & (1 << index)) != 0; 268 } 269 } 270 271 /** 272 * Constructor. 273 * 274 * @param commonPrefixes comma-separated list of common prefixes, 275 * e.g. "Mr, Ms, Mrs" 276 * @param commonLastNamePrefixes comma-separated list of common last name prefixes, 277 * e.g. "d', st, st., von" 278 * @param commonSuffixes comma-separated list of common suffixes, 279 * e.g. "Jr, M.D., MD, D.D.S." 280 * @param commonConjunctions comma-separated list of common conjuctions, 281 * e.g. "AND, Or" 282 */ NameSplitter(String commonPrefixes, String commonLastNamePrefixes, String commonSuffixes, String commonConjunctions, Locale locale)283 public NameSplitter(String commonPrefixes, String commonLastNamePrefixes, 284 String commonSuffixes, String commonConjunctions, Locale locale) { 285 // TODO: refactor this to use <string-array> resources 286 mPrefixesSet = convertToSet(commonPrefixes); 287 mLastNamePrefixesSet = convertToSet(commonLastNamePrefixes); 288 mSuffixesSet = convertToSet(commonSuffixes); 289 mConjuctions = convertToSet(commonConjunctions); 290 mLocale = locale != null ? locale : Locale.getDefault(); 291 mLanguage = mLocale.getLanguage().toLowerCase(); 292 293 int maxLength = 0; 294 for (String suffix : mSuffixesSet) { 295 if (suffix.length() > maxLength) { 296 maxLength = suffix.length(); 297 } 298 } 299 300 mMaxSuffixLength = maxLength; 301 } 302 303 /** 304 * Converts a comma-separated list of Strings to a set of Strings. Trims strings 305 * and converts them to upper case. 306 */ convertToSet(String strings)307 private static ArraySet<String> convertToSet(String strings) { 308 ArraySet<String> set = new ArraySet<>(); 309 if (strings != null) { 310 String[] split = strings.split(","); 311 for (int i = 0; i < split.length; i++) { 312 set.add(split[i].trim().toUpperCase()); 313 } 314 } 315 return set; 316 } 317 318 /** 319 * Parses a full name and returns components as a list of tokens. 320 */ tokenize(String[] tokens, String fullName)321 public int tokenize(String[] tokens, String fullName) { 322 if (fullName == null) { 323 return 0; 324 } 325 326 NameTokenizer tokenizer = new NameTokenizer(fullName); 327 328 if (tokenizer.mStartPointer == tokenizer.mEndPointer) { 329 return 0; 330 } 331 332 String firstToken = tokenizer.mTokens[tokenizer.mStartPointer]; 333 int count = 0; 334 for (int i = tokenizer.mStartPointer; i < tokenizer.mEndPointer; i++) { 335 tokens[count++] = tokenizer.mTokens[i]; 336 } 337 338 return count; 339 } 340 341 342 /** 343 * Parses a full name and returns parsed components in the Name object. 344 */ split(Name name, String fullName)345 public void split(Name name, String fullName) { 346 if (fullName == null) { 347 return; 348 } 349 350 int fullNameStyle = guessFullNameStyle(fullName); 351 if (fullNameStyle == FullNameStyle.CJK) { 352 fullNameStyle = getAdjustedFullNameStyle(fullNameStyle); 353 } 354 355 split(name, fullName, fullNameStyle); 356 } 357 358 /** 359 * Parses a full name and returns parsed components in the Name object 360 * with a given fullNameStyle. 361 */ split(Name name, String fullName, int fullNameStyle)362 public void split(Name name, String fullName, int fullNameStyle) { 363 if (fullName == null) { 364 return; 365 } 366 367 name.fullNameStyle = fullNameStyle; 368 369 switch (fullNameStyle) { 370 case FullNameStyle.CHINESE: 371 splitChineseName(name, fullName); 372 break; 373 374 case FullNameStyle.JAPANESE: 375 splitJapaneseName(name, fullName); 376 break; 377 378 case FullNameStyle.KOREAN: 379 splitKoreanName(name, fullName); 380 break; 381 382 default: 383 splitWesternName(name, fullName); 384 } 385 } 386 387 /** 388 * Splits a full name composed according to the Western tradition: 389 * <pre> 390 * [prefix] given name(s) [[middle name] family name] [, suffix] 391 * [prefix] family name, given name [middle name] [,suffix] 392 * </pre> 393 */ splitWesternName(Name name, String fullName)394 private void splitWesternName(Name name, String fullName) { 395 NameTokenizer tokens = new NameTokenizer(fullName); 396 parsePrefix(name, tokens); 397 398 // If the name consists of just one or two tokens, treat them as first/last name, 399 // not as suffix. Example: John Ma; Ma is last name, not "M.A.". 400 if (tokens.mEndPointer > 2) { 401 parseSuffix(name, tokens); 402 } 403 404 if (name.prefix == null && tokens.mEndPointer - tokens.mStartPointer == 1) { 405 name.givenNames = tokens.mTokens[tokens.mStartPointer]; 406 } else { 407 parseLastName(name, tokens); 408 parseMiddleName(name, tokens); 409 parseGivenNames(name, tokens); 410 } 411 } 412 413 /** 414 * Splits a full name composed according to the Chinese tradition: 415 * <pre> 416 * [family name [middle name]] given name 417 * </pre> 418 */ splitChineseName(Name name, String fullName)419 private void splitChineseName(Name name, String fullName) { 420 StringTokenizer tokenizer = new StringTokenizer(fullName); 421 while (tokenizer.hasMoreTokens()) { 422 String token = tokenizer.nextToken(); 423 if (name.givenNames == null) { 424 name.givenNames = token; 425 } else if (name.familyName == null) { 426 name.familyName = name.givenNames; 427 name.givenNames = token; 428 } else if (name.middleName == null) { 429 name.middleName = name.givenNames; 430 name.givenNames = token; 431 } else { 432 name.middleName = name.middleName + name.givenNames; 433 name.givenNames = token; 434 } 435 } 436 437 // If a single word parse that word up. 438 if (name.givenNames != null && name.familyName == null && name.middleName == null) { 439 int length = fullName.length(); 440 if (length == 2) { 441 name.familyName = fullName.substring(0, 1); 442 name.givenNames = fullName.substring(1); 443 } else if (length == 3) { 444 name.familyName = fullName.substring(0, 1); 445 name.middleName = fullName.substring(1, 2); 446 name.givenNames = fullName.substring(2); 447 } else if (length == 4) { 448 name.familyName = fullName.substring(0, 2); 449 name.middleName = fullName.substring(2, 3); 450 name.givenNames = fullName.substring(3); 451 } 452 453 } 454 } 455 456 /** 457 * Splits a full name composed according to the Japanese tradition: 458 * <pre> 459 * [family name] given name(s) 460 * </pre> 461 */ splitJapaneseName(Name name, String fullName)462 private void splitJapaneseName(Name name, String fullName) { 463 StringTokenizer tokenizer = new StringTokenizer(fullName); 464 while (tokenizer.hasMoreTokens()) { 465 String token = tokenizer.nextToken(); 466 if (name.givenNames == null) { 467 name.givenNames = token; 468 } else if (name.familyName == null) { 469 name.familyName = name.givenNames; 470 name.givenNames = token; 471 } else { 472 name.givenNames += " " + token; 473 } 474 } 475 } 476 477 /** 478 * Splits a full name composed according to the Korean tradition: 479 * <pre> 480 * [family name] given name(s) 481 * </pre> 482 */ splitKoreanName(Name name, String fullName)483 private void splitKoreanName(Name name, String fullName) { 484 StringTokenizer tokenizer = new StringTokenizer(fullName); 485 if (tokenizer.countTokens() > 1) { 486 // Each name can be identified by separators. 487 while (tokenizer.hasMoreTokens()) { 488 String token = tokenizer.nextToken(); 489 if (name.givenNames == null) { 490 name.givenNames = token; 491 } else if (name.familyName == null) { 492 name.familyName = name.givenNames; 493 name.givenNames = token; 494 } else { 495 name.givenNames += " " + token; 496 } 497 } 498 } else { 499 // There is no separator. Try to guess family name. 500 // The length of most family names is 1. 501 int familyNameLength = 1; 502 503 // Compare with 2-length family names. 504 for (String twoLengthFamilyName : KOREAN_TWO_CHARCTER_FAMILY_NAMES) { 505 if (fullName.startsWith(twoLengthFamilyName)) { 506 familyNameLength = 2; 507 break; 508 } 509 } 510 511 name.familyName = fullName.substring(0, familyNameLength); 512 if (fullName.length() > familyNameLength) { 513 name.givenNames = fullName.substring(familyNameLength); 514 } 515 } 516 } 517 518 /** 519 * Concatenates components of a name according to the rules dictated by the name style. 520 * 521 * @param givenNameFirst is ignored for CJK display name styles 522 */ join(Name name, boolean givenNameFirst, boolean includePrefix)523 public String join(Name name, boolean givenNameFirst, boolean includePrefix) { 524 String prefix = includePrefix ? name.prefix : null; 525 switch (name.fullNameStyle) { 526 case FullNameStyle.CJK: 527 case FullNameStyle.CHINESE: 528 case FullNameStyle.KOREAN: 529 return join(prefix, name.familyName, name.middleName, name.givenNames, 530 name.suffix, false, false, false); 531 532 case FullNameStyle.JAPANESE: 533 return join(prefix, name.familyName, name.middleName, name.givenNames, 534 name.suffix, true, false, false); 535 536 default: 537 if (givenNameFirst) { 538 return join(prefix, name.givenNames, name.middleName, name.familyName, 539 name.suffix, true, false, true); 540 } else { 541 return join(prefix, name.familyName, name.givenNames, name.middleName, 542 name.suffix, true, true, true); 543 } 544 } 545 } 546 547 /** 548 * Concatenates components of the phonetic name following the CJK tradition: 549 * family name + middle name + given name(s). 550 */ joinPhoneticName(Name name)551 public String joinPhoneticName(Name name) { 552 return join(null, name.phoneticFamilyName, 553 name.phoneticMiddleName, name.phoneticGivenName, null, true, false, false); 554 } 555 556 /** 557 * Concatenates parts of a full name inserting spaces and commas as specified. 558 */ join(String prefix, String part1, String part2, String part3, String suffix, boolean useSpace, boolean useCommaAfterPart1, boolean useCommaAfterPart3)559 private String join(String prefix, String part1, String part2, String part3, String suffix, 560 boolean useSpace, boolean useCommaAfterPart1, boolean useCommaAfterPart3) { 561 prefix = prefix == null ? null: prefix.trim(); 562 part1 = part1 == null ? null: part1.trim(); 563 part2 = part2 == null ? null: part2.trim(); 564 part3 = part3 == null ? null: part3.trim(); 565 suffix = suffix == null ? null: suffix.trim(); 566 567 boolean hasPrefix = !TextUtils.isEmpty(prefix); 568 boolean hasPart1 = !TextUtils.isEmpty(part1); 569 boolean hasPart2 = !TextUtils.isEmpty(part2); 570 boolean hasPart3 = !TextUtils.isEmpty(part3); 571 boolean hasSuffix = !TextUtils.isEmpty(suffix); 572 573 boolean isSingleWord = true; 574 String singleWord = null; 575 576 if (hasPrefix) { 577 singleWord = prefix; 578 } 579 580 if (hasPart1) { 581 if (singleWord != null) { 582 isSingleWord = false; 583 } else { 584 singleWord = part1; 585 } 586 } 587 588 if (hasPart2) { 589 if (singleWord != null) { 590 isSingleWord = false; 591 } else { 592 singleWord = part2; 593 } 594 } 595 596 if (hasPart3) { 597 if (singleWord != null) { 598 isSingleWord = false; 599 } else { 600 singleWord = part3; 601 } 602 } 603 604 if (hasSuffix) { 605 if (singleWord != null) { 606 isSingleWord = false; 607 } else { 608 singleWord = normalizedSuffix(suffix); 609 } 610 } 611 612 if (isSingleWord) { 613 return singleWord; 614 } 615 616 StringBuilder sb = new StringBuilder(); 617 618 if (hasPrefix) { 619 sb.append(prefix); 620 } 621 622 if (hasPart1) { 623 if (hasPrefix) { 624 sb.append(' '); 625 } 626 sb.append(part1); 627 } 628 629 if (hasPart2) { 630 if (hasPrefix || hasPart1) { 631 if (useCommaAfterPart1) { 632 sb.append(','); 633 } 634 if (useSpace) { 635 sb.append(' '); 636 } 637 } 638 sb.append(part2); 639 } 640 641 if (hasPart3) { 642 if (hasPrefix || hasPart1 || hasPart2) { 643 if (useSpace) { 644 sb.append(' '); 645 } 646 } 647 sb.append(part3); 648 } 649 650 if (hasSuffix) { 651 if (hasPrefix || hasPart1 || hasPart2 || hasPart3) { 652 if (useCommaAfterPart3) { 653 sb.append(','); 654 } 655 if (useSpace) { 656 sb.append(' '); 657 } 658 } 659 sb.append(normalizedSuffix(suffix)); 660 } 661 662 return sb.toString(); 663 } 664 665 /** 666 * Puts a dot after the supplied suffix if that is the accepted form of the suffix, 667 * e.g. "Jr." and "Sr.", but not "I", "II" and "III". 668 */ normalizedSuffix(String suffix)669 private String normalizedSuffix(String suffix) { 670 int length = suffix.length(); 671 if (length == 0 || suffix.charAt(length - 1) == '.') { 672 return suffix; 673 } 674 675 String withDot = suffix + '.'; 676 if (mSuffixesSet.contains(withDot.toUpperCase())) { 677 return withDot; 678 } else { 679 return suffix; 680 } 681 } 682 683 /** 684 * If the supplied name style is undefined, returns a default based on the language, 685 * otherwise returns the supplied name style itself. 686 * 687 * @param nameStyle See {@link FullNameStyle}. 688 */ getAdjustedFullNameStyle(int nameStyle)689 public int getAdjustedFullNameStyle(int nameStyle) { 690 if (nameStyle == FullNameStyle.UNDEFINED) { 691 if (JAPANESE_LANGUAGE.equals(mLanguage)) { 692 return FullNameStyle.JAPANESE; 693 } else if (KOREAN_LANGUAGE.equals(mLanguage)) { 694 return FullNameStyle.KOREAN; 695 } else if (CHINESE_LANGUAGE.equals(mLanguage)) { 696 return FullNameStyle.CHINESE; 697 } else { 698 return FullNameStyle.WESTERN; 699 } 700 } else if (nameStyle == FullNameStyle.CJK) { 701 if (JAPANESE_LANGUAGE.equals(mLanguage)) { 702 return FullNameStyle.JAPANESE; 703 } else if (KOREAN_LANGUAGE.equals(mLanguage)) { 704 return FullNameStyle.KOREAN; 705 } else { 706 return FullNameStyle.CHINESE; 707 } 708 } 709 return nameStyle; 710 } 711 712 /** 713 * Parses the first word from the name if it is a prefix. 714 */ parsePrefix(Name name, NameTokenizer tokens)715 private void parsePrefix(Name name, NameTokenizer tokens) { 716 if (tokens.mStartPointer == tokens.mEndPointer) { 717 return; 718 } 719 720 String firstToken = tokens.mTokens[tokens.mStartPointer]; 721 if (mPrefixesSet.contains(firstToken.toUpperCase())) { 722 if (tokens.hasDot(tokens.mStartPointer)) { 723 firstToken += '.'; 724 } 725 name.prefix = firstToken; 726 tokens.mStartPointer++; 727 } 728 } 729 730 /** 731 * Parses the last word(s) from the name if it is a suffix. 732 */ parseSuffix(Name name, NameTokenizer tokens)733 private void parseSuffix(Name name, NameTokenizer tokens) { 734 if (tokens.mStartPointer == tokens.mEndPointer) { 735 return; 736 } 737 738 String lastToken = tokens.mTokens[tokens.mEndPointer - 1]; 739 740 // Take care of an explicit comma-separated suffix 741 if (tokens.mEndPointer - tokens.mStartPointer > 2 742 && tokens.hasComma(tokens.mEndPointer - 2)) { 743 if (tokens.hasDot(tokens.mEndPointer - 1)) { 744 lastToken += '.'; 745 } 746 name.suffix = lastToken; 747 tokens.mEndPointer--; 748 return; 749 } 750 751 if (lastToken.length() > mMaxSuffixLength) { 752 return; 753 } 754 755 String normalized = lastToken.toUpperCase(); 756 if (mSuffixesSet.contains(normalized)) { 757 name.suffix = lastToken; 758 tokens.mEndPointer--; 759 return; 760 } 761 762 if (tokens.hasDot(tokens.mEndPointer - 1)) { 763 lastToken += '.'; 764 } 765 normalized += "."; 766 767 // Take care of suffixes like M.D. and D.D.S. 768 int pos = tokens.mEndPointer - 1; 769 while (normalized.length() <= mMaxSuffixLength) { 770 771 if (mSuffixesSet.contains(normalized)) { 772 name.suffix = lastToken; 773 tokens.mEndPointer = pos; 774 return; 775 } 776 777 if (pos == tokens.mStartPointer) { 778 break; 779 } 780 781 pos--; 782 if (tokens.hasDot(pos)) { 783 lastToken = tokens.mTokens[pos] + "." + lastToken; 784 } else { 785 lastToken = tokens.mTokens[pos] + " " + lastToken; 786 } 787 788 normalized = tokens.mTokens[pos].toUpperCase() + "." + normalized; 789 } 790 } 791 parseLastName(Name name, NameTokenizer tokens)792 private void parseLastName(Name name, NameTokenizer tokens) { 793 if (tokens.mStartPointer == tokens.mEndPointer) { 794 return; 795 } 796 797 // If the first word is followed by a comma, assume that it's the family name 798 if (tokens.hasComma(tokens.mStartPointer)) { 799 name.familyName = tokens.mTokens[tokens.mStartPointer]; 800 tokens.mStartPointer++; 801 return; 802 } 803 804 // If the second word is followed by a comma and the first word 805 // is a last name prefix as in "de Sade" and "von Cliburn", treat 806 // the first two words as the family name. 807 if (tokens.mStartPointer + 1 < tokens.mEndPointer 808 && tokens.hasComma(tokens.mStartPointer + 1) 809 && isFamilyNamePrefix(tokens.mTokens[tokens.mStartPointer])) { 810 String familyNamePrefix = tokens.mTokens[tokens.mStartPointer]; 811 if (tokens.hasDot(tokens.mStartPointer)) { 812 familyNamePrefix += '.'; 813 } 814 name.familyName = familyNamePrefix + " " + tokens.mTokens[tokens.mStartPointer + 1]; 815 tokens.mStartPointer += 2; 816 return; 817 } 818 819 // Finally, assume that the last word is the last name 820 name.familyName = tokens.mTokens[tokens.mEndPointer - 1]; 821 tokens.mEndPointer--; 822 823 // Take care of last names like "de Sade" and "von Cliburn" 824 if ((tokens.mEndPointer - tokens.mStartPointer) > 0) { 825 String lastNamePrefix = tokens.mTokens[tokens.mEndPointer - 1]; 826 if (isFamilyNamePrefix(lastNamePrefix)) { 827 if (tokens.hasDot(tokens.mEndPointer - 1)) { 828 lastNamePrefix += '.'; 829 } 830 name.familyName = lastNamePrefix + " " + name.familyName; 831 tokens.mEndPointer--; 832 } 833 } 834 } 835 836 /** 837 * Returns true if the supplied word is an accepted last name prefix, e.g. "von", "de" 838 */ isFamilyNamePrefix(String word)839 private boolean isFamilyNamePrefix(String word) { 840 final String normalized = word.toUpperCase(); 841 842 return mLastNamePrefixesSet.contains(normalized) 843 || mLastNamePrefixesSet.contains(normalized + "."); 844 } 845 846 parseMiddleName(Name name, NameTokenizer tokens)847 private void parseMiddleName(Name name, NameTokenizer tokens) { 848 if (tokens.mStartPointer == tokens.mEndPointer) { 849 return; 850 } 851 852 if ((tokens.mEndPointer - tokens.mStartPointer) > 1) { 853 if ((tokens.mEndPointer - tokens.mStartPointer) == 2 854 || !mConjuctions.contains(tokens.mTokens[tokens.mEndPointer - 2]. 855 toUpperCase())) { 856 name.middleName = tokens.mTokens[tokens.mEndPointer - 1]; 857 if (tokens.hasDot(tokens.mEndPointer - 1)) { 858 name.middleName += '.'; 859 } 860 tokens.mEndPointer--; 861 } 862 } 863 } 864 parseGivenNames(Name name, NameTokenizer tokens)865 private void parseGivenNames(Name name, NameTokenizer tokens) { 866 if (tokens.mStartPointer == tokens.mEndPointer) { 867 return; 868 } 869 870 if ((tokens.mEndPointer - tokens.mStartPointer) == 1) { 871 name.givenNames = tokens.mTokens[tokens.mStartPointer]; 872 } else { 873 StringBuilder sb = new StringBuilder(); 874 for (int i = tokens.mStartPointer; i < tokens.mEndPointer; i++) { 875 if (i != tokens.mStartPointer) { 876 sb.append(' '); 877 } 878 sb.append(tokens.mTokens[i]); 879 if (tokens.hasDot(i)) { 880 sb.append('.'); 881 } 882 } 883 name.givenNames = sb.toString(); 884 } 885 } 886 887 /** 888 * Makes the best guess at the expected full name style based on the character set 889 * used in the supplied name. If the phonetic name is also supplied, tries to 890 * differentiate between Chinese, Japanese and Korean based on the alphabet used 891 * for the phonetic name. 892 */ guessNameStyle(Name name)893 public void guessNameStyle(Name name) { 894 guessFullNameStyle(name); 895 guessPhoneticNameStyle(name); 896 name.fullNameStyle = getAdjustedNameStyleBasedOnPhoneticNameStyle(name.fullNameStyle, 897 name.phoneticNameStyle); 898 } 899 900 /** 901 * Updates the display name style according to the phonetic name style if we 902 * were unsure about display name style based on the name components, but 903 * phonetic name makes it more definitive. 904 */ getAdjustedNameStyleBasedOnPhoneticNameStyle(int nameStyle, int phoneticNameStyle)905 public int getAdjustedNameStyleBasedOnPhoneticNameStyle(int nameStyle, int phoneticNameStyle) { 906 if (phoneticNameStyle != PhoneticNameStyle.UNDEFINED) { 907 if (nameStyle == FullNameStyle.UNDEFINED || nameStyle == FullNameStyle.CJK) { 908 if (phoneticNameStyle == PhoneticNameStyle.JAPANESE) { 909 return FullNameStyle.JAPANESE; 910 } else if (phoneticNameStyle == PhoneticNameStyle.KOREAN) { 911 return FullNameStyle.KOREAN; 912 } 913 if (nameStyle == FullNameStyle.CJK && phoneticNameStyle == PhoneticNameStyle.PINYIN) { 914 return FullNameStyle.CHINESE; 915 } 916 } 917 } 918 return nameStyle; 919 } 920 921 /** 922 * Makes the best guess at the expected full name style based on the character set 923 * used in the supplied name. 924 */ guessFullNameStyle(NameSplitter.Name name)925 private void guessFullNameStyle(NameSplitter.Name name) { 926 if (name.fullNameStyle != FullNameStyle.UNDEFINED) { 927 return; 928 } 929 930 int bestGuess = guessFullNameStyle(name.givenNames); 931 // A mix of Hanzi and latin chars are common in China, so we have to go through all names 932 // if the name is not JANPANESE or KOREAN. 933 if (bestGuess != FullNameStyle.UNDEFINED && bestGuess != FullNameStyle.CJK 934 && bestGuess != FullNameStyle.WESTERN) { 935 name.fullNameStyle = bestGuess; 936 return; 937 } 938 939 int guess = guessFullNameStyle(name.familyName); 940 if (guess != FullNameStyle.UNDEFINED) { 941 if (guess != FullNameStyle.CJK && guess != FullNameStyle.WESTERN) { 942 name.fullNameStyle = guess; 943 return; 944 } 945 bestGuess = guess; 946 } 947 948 name.fullNameStyle = bestGuess; 949 } 950 guessFullNameStyle(String name)951 public int guessFullNameStyle(String name) { 952 if (name == null) { 953 return FullNameStyle.UNDEFINED; 954 } 955 956 int nameStyle = FullNameStyle.UNDEFINED; 957 int length = name.length(); 958 int offset = 0; 959 while (offset < length) { 960 int codePoint = Character.codePointAt(name, offset); 961 if (Character.isLetter(codePoint)) { 962 UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint); 963 964 if (!isLatinUnicodeBlock(unicodeBlock)) { 965 966 if (isCJKUnicodeBlock(unicodeBlock)) { 967 // We don't know if this is Chinese, Japanese or Korean - 968 // trying to figure out by looking at other characters in the name 969 return guessCJKNameStyle(name, offset + Character.charCount(codePoint)); 970 } 971 972 if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) { 973 return FullNameStyle.JAPANESE; 974 } 975 976 if (isKoreanUnicodeBlock(unicodeBlock)) { 977 return FullNameStyle.KOREAN; 978 } 979 } 980 nameStyle = FullNameStyle.WESTERN; 981 } 982 offset += Character.charCount(codePoint); 983 } 984 return nameStyle; 985 } 986 guessCJKNameStyle(String name, int offset)987 private int guessCJKNameStyle(String name, int offset) { 988 int length = name.length(); 989 while (offset < length) { 990 int codePoint = Character.codePointAt(name, offset); 991 if (Character.isLetter(codePoint)) { 992 UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint); 993 if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) { 994 return FullNameStyle.JAPANESE; 995 } 996 if (isKoreanUnicodeBlock(unicodeBlock)) { 997 return FullNameStyle.KOREAN; 998 } 999 } 1000 offset += Character.charCount(codePoint); 1001 } 1002 1003 return FullNameStyle.CJK; 1004 } 1005 guessPhoneticNameStyle(NameSplitter.Name name)1006 private void guessPhoneticNameStyle(NameSplitter.Name name) { 1007 if (name.phoneticNameStyle != PhoneticNameStyle.UNDEFINED) { 1008 return; 1009 } 1010 1011 int bestGuess = guessPhoneticNameStyle(name.phoneticFamilyName); 1012 if (bestGuess != FullNameStyle.UNDEFINED && bestGuess != FullNameStyle.CJK) { 1013 name.phoneticNameStyle = bestGuess; 1014 return; 1015 } 1016 1017 int guess = guessPhoneticNameStyle(name.phoneticGivenName); 1018 if (guess != FullNameStyle.UNDEFINED) { 1019 if (guess != FullNameStyle.CJK) { 1020 name.phoneticNameStyle = guess; 1021 return; 1022 } 1023 bestGuess = guess; 1024 } 1025 1026 guess = guessPhoneticNameStyle(name.phoneticMiddleName); 1027 if (guess != FullNameStyle.UNDEFINED) { 1028 if (guess != FullNameStyle.CJK) { 1029 name.phoneticNameStyle = guess; 1030 return; 1031 } 1032 bestGuess = guess; 1033 } 1034 } 1035 guessPhoneticNameStyle(String name)1036 public int guessPhoneticNameStyle(String name) { 1037 if (name == null) { 1038 return PhoneticNameStyle.UNDEFINED; 1039 } 1040 1041 int nameStyle = PhoneticNameStyle.UNDEFINED; 1042 int length = name.length(); 1043 int offset = 0; 1044 while (offset < length) { 1045 int codePoint = Character.codePointAt(name, offset); 1046 if (Character.isLetter(codePoint)) { 1047 UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint); 1048 if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) { 1049 return PhoneticNameStyle.JAPANESE; 1050 } 1051 if (isKoreanUnicodeBlock(unicodeBlock)) { 1052 return PhoneticNameStyle.KOREAN; 1053 } 1054 if (isLatinUnicodeBlock(unicodeBlock)) { 1055 return PhoneticNameStyle.PINYIN; 1056 } 1057 } 1058 offset += Character.charCount(codePoint); 1059 } 1060 1061 return nameStyle; 1062 } 1063 isLatinUnicodeBlock(UnicodeBlock unicodeBlock)1064 private static boolean isLatinUnicodeBlock(UnicodeBlock unicodeBlock) { 1065 return unicodeBlock == UnicodeBlock.BASIC_LATIN || 1066 unicodeBlock == UnicodeBlock.LATIN_1_SUPPLEMENT || 1067 unicodeBlock == UnicodeBlock.LATIN_EXTENDED_A || 1068 unicodeBlock == UnicodeBlock.LATIN_EXTENDED_B || 1069 unicodeBlock == UnicodeBlock.LATIN_EXTENDED_ADDITIONAL; 1070 } 1071 isCJKUnicodeBlock(UnicodeBlock block)1072 private static boolean isCJKUnicodeBlock(UnicodeBlock block) { 1073 return block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS 1074 || block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A 1075 || block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B 1076 || block == UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION 1077 || block == UnicodeBlock.CJK_RADICALS_SUPPLEMENT 1078 || block == UnicodeBlock.CJK_COMPATIBILITY 1079 || block == UnicodeBlock.CJK_COMPATIBILITY_FORMS 1080 || block == UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS 1081 || block == UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT; 1082 } 1083 isKoreanUnicodeBlock(UnicodeBlock unicodeBlock)1084 private static boolean isKoreanUnicodeBlock(UnicodeBlock unicodeBlock) { 1085 return unicodeBlock == UnicodeBlock.HANGUL_SYLLABLES || 1086 unicodeBlock == UnicodeBlock.HANGUL_JAMO || 1087 unicodeBlock == UnicodeBlock.HANGUL_COMPATIBILITY_JAMO; 1088 } 1089 isJapanesePhoneticUnicodeBlock(UnicodeBlock unicodeBlock)1090 private static boolean isJapanesePhoneticUnicodeBlock(UnicodeBlock unicodeBlock) { 1091 return unicodeBlock == UnicodeBlock.KATAKANA || 1092 unicodeBlock == UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS || 1093 unicodeBlock == UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS || 1094 unicodeBlock == UnicodeBlock.HIRAGANA; 1095 } 1096 } 1097