|
22 | 22 | import java.io.Serializable; |
23 | 23 | import java.util.*; |
24 | 24 | import java.util.concurrent.ExecutionException; |
| 25 | +import java.util.regex.Matcher; |
| 26 | +import java.util.regex.Pattern; |
25 | 27 | import java.util.stream.Collectors; |
26 | 28 | import javax.swing.Icon; |
27 | 29 | import net.rptools.maptool.client.AppPreferences; |
@@ -476,59 +478,30 @@ public void update() { |
476 | 478 |
|
477 | 479 | /** |
478 | 480 | * Sort the tokens by their initiative state according to the default, descending order. See |
479 | | - * {@link #sort(boolean)} for more details on handling of strings and nulls. |
| 481 | + * {@link #sort(boolean)} for more details. |
| 482 | + * |
| 483 | + * @see TokenInitiativeDescComparator |
480 | 484 | */ |
481 | 485 | public void sort() { |
482 | 486 | this.sort(false); |
483 | 487 | } |
484 | 488 |
|
485 | 489 | /** |
486 | | - * Sort the tokens by their initiative state, in either ascending or descending order. If the |
487 | | - * initiative state string can be converted into a {@link Double} that is done first. All values |
488 | | - * converted to {@link Double}s are always considered bigger than the {@link String} values. The |
489 | | - * {@link String} values are considered bigger than any <code>null</code> values. |
| 490 | + * Sort the tokens by their initiative state using a {@link Comparator} |
| 491 | + * |
| 492 | + * @param ascendingOrder |
| 493 | + * @see TokenInitiativeDescComparator |
490 | 494 | */ |
491 | 495 | public void sort(boolean ascendingOrder) { |
492 | 496 | startUnitOfWork(); |
493 | | - final int DIRECTION = ascendingOrder ? -1 : 1; |
494 | 497 | TokenInitiative currentInitiative = |
495 | 498 | getTokenInitiative(getCurrent()); // Save the currently selected initiative |
496 | | - tokens.sort( |
497 | | - (o1, o2) -> { |
498 | | - |
499 | | - // Get a number, string, or null for first parameter |
500 | | - Object one = null; |
501 | | - if (o1.state != null) { |
502 | | - one = o1.state; |
503 | | - try { |
504 | | - one = Double.valueOf(o1.state); |
505 | | - } catch (NumberFormatException e) { |
506 | | - // Not a number so ignore |
507 | | - } // endtry |
508 | | - } // endif |
509 | | - |
510 | | - // Repeat for second param |
511 | | - Object two = null; |
512 | | - if (o2.state != null) { |
513 | | - two = o2.state; |
514 | | - try { |
515 | | - two = Double.valueOf(o2.state); |
516 | | - } catch (NumberFormatException e) { |
517 | | - // Not a number so ignore |
518 | | - } // endtry |
519 | | - } // endif |
520 | | - |
521 | | - // Do the comparison |
522 | | - if (Objects.equals(one, two)) return 0; |
523 | | - if (one == null) return 1 * DIRECTION; // Null is always the smallest value |
524 | | - if (two == null) return -1 * DIRECTION; |
525 | | - if (one instanceof Double & two instanceof Double) |
526 | | - return ((Double) two).compareTo((Double) one) * DIRECTION; |
527 | | - if (one instanceof String & two instanceof String) |
528 | | - return ((String) two).compareTo((String) one) * DIRECTION; |
529 | | - if (one instanceof Double) return -1 * DIRECTION; // Integers are bigger than strings |
530 | | - return 1 * DIRECTION; |
531 | | - }); |
| 499 | + |
| 500 | + Collections.sort(tokens, new TokenInitiativeDescComparator()); |
| 501 | + if (ascendingOrder) { |
| 502 | + Collections.reverse(tokens); |
| 503 | + } |
| 504 | + |
532 | 505 | getPCS().firePropertyChange(TOKENS_PROP, null, tokens); |
533 | 506 | setCurrent(indexOf(currentInitiative)); // Restore current initiative |
534 | 507 | finishUnitOfWork(); |
@@ -1001,4 +974,100 @@ public TokenInitiativeDto toDto() { |
1001 | 974 | return dto.build(); |
1002 | 975 | } |
1003 | 976 | } |
| 977 | + |
| 978 | + /*--------------------------------------------------------------------------------------------- |
| 979 | + * TokenInitiativeComparator Inner Class |
| 980 | + *-------------------------------------------------------------------------------------------*/ |
| 981 | + |
| 982 | + /** |
| 983 | + * Comparator in a helper class to control the ordering of the TokenInitiative list. |
| 984 | + * |
| 985 | + * <p>Uses regex {@link Pattern} and {@link Matcher} to try and extract a leading number (to |
| 986 | + * convert to a {@link Double}) and/or a trailing string from the token's initiative state. Then |
| 987 | + * it compares in descending fashion by: |
| 988 | + * |
| 989 | + * <ol> |
| 990 | + * <li>{@link Double}s are always considered bigger than the {@link String} values. |
| 991 | + * <li>{@link String} values are considered bigger than any <code>null</code> values. |
| 992 | + * <li>if all else is the same, finally compare by token name and then by token ID to get a |
| 993 | + * deterministic order to prevent flip-flopping on repeated sorts and/or tokens not changing |
| 994 | + * order when sort order is reversed. |
| 995 | + * </ol> |
| 996 | + * |
| 997 | + * @see TokenInitiative |
| 998 | + */ |
| 999 | + public class TokenInitiativeDescComparator implements Comparator<TokenInitiative> { |
| 1000 | + @Override |
| 1001 | + public int compare(TokenInitiative o1, TokenInitiative o2) { |
| 1002 | + Pattern p = Pattern.compile("^(-?\\d+\\.?\\d*)?(.*)"); |
| 1003 | + |
| 1004 | + // Get a number and/or string, or a null for first parameter |
| 1005 | + Object one = o1.getState(); |
| 1006 | + Double oneDouble = null; |
| 1007 | + String oneString = null; |
| 1008 | + try { |
| 1009 | + Matcher m1 = p.matcher(o1.state); |
| 1010 | + if (m1.matches()) { |
| 1011 | + oneString = m1.group(2).trim(); |
| 1012 | + oneDouble = Double.valueOf(m1.group(1)); |
| 1013 | + } |
| 1014 | + } catch (NumberFormatException | NullPointerException e) { |
| 1015 | + // either null or could not convert to a Double |
| 1016 | + } |
| 1017 | + |
| 1018 | + // Repeat for second param |
| 1019 | + Object two = o2.getState(); |
| 1020 | + Double twoDouble = null; |
| 1021 | + String twoString = null; |
| 1022 | + try { |
| 1023 | + Matcher m2 = p.matcher(o2.state); |
| 1024 | + if (m2.matches()) { |
| 1025 | + twoString = m2.group(2).trim(); |
| 1026 | + twoDouble = Double.valueOf(m2.group(1)); |
| 1027 | + } |
| 1028 | + } catch (NumberFormatException | NullPointerException e) { |
| 1029 | + // either null or could not convert to a Double |
| 1030 | + } |
| 1031 | + |
| 1032 | + int comparison = 0; |
| 1033 | + if (Objects.equals(one, two)) { |
| 1034 | + comparison = 0; |
| 1035 | + } else if (one == null) { |
| 1036 | + comparison = 1; // nulls come last |
| 1037 | + } else if (two == null) { |
| 1038 | + comparison = -1; |
| 1039 | + } else if (!Objects.equals(twoDouble, oneDouble)) { |
| 1040 | + // Numbers are different |
| 1041 | + if (oneDouble == null) { |
| 1042 | + comparison = 1; |
| 1043 | + } else if (twoDouble == null) { |
| 1044 | + comparison = -1; |
| 1045 | + } else { |
| 1046 | + comparison = twoDouble.compareTo(oneDouble); |
| 1047 | + } |
| 1048 | + } else { |
| 1049 | + // Numbers are the same, so sort by strings |
| 1050 | + if (oneString == null) { |
| 1051 | + comparison = 1; |
| 1052 | + } else if (twoString == null) { |
| 1053 | + comparison = -1; |
| 1054 | + } else { |
| 1055 | + comparison = twoString.compareToIgnoreCase(oneString); |
| 1056 | + if (comparison == 0) { |
| 1057 | + comparison = twoString.compareTo(oneString); |
| 1058 | + } |
| 1059 | + } |
| 1060 | + } |
| 1061 | + |
| 1062 | + // If still tied, compare the names and then the GUID to get a deterministic sort order. |
| 1063 | + if (comparison == 0) { |
| 1064 | + comparison = o1.getToken().getName().compareTo(o2.getToken().getName()); |
| 1065 | + if (comparison == 0) { |
| 1066 | + comparison = o1.getToken().getId().compareTo(o2.getToken().getId()); |
| 1067 | + } |
| 1068 | + } |
| 1069 | + |
| 1070 | + return comparison; |
| 1071 | + } |
| 1072 | + } |
1004 | 1073 | } |
0 commit comments