aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/cowtipper/cowlection/util/Utils.java
blob: 2db72d16e17d272a9fd601ae111684c7fa150468 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
package de.cowtipper.cowlection.util;

import com.mojang.realmsclient.util.Pair;
import de.cowtipper.cowlection.Cowlection;
import de.cowtipper.cowlection.config.MooConfig;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.EnumChatFormatting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public final class Utils {
    public static final Pattern VALID_UUID_PATTERN = Pattern.compile("^(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$");
    private static final Pattern VALID_USERNAME = Pattern.compile("^[\\w]{1,16}$");
    private static final Pattern ALTERNATE_COLOR_CODES_PATTERN = Pattern.compile("&([0-9a-fk-or])");
    private static final Pattern MC_COLOR_CODES_PATTERN = Pattern.compile("§([0-9a-fk-or])");
    private static final NavigableMap<Double, Character> NUMBER_SUFFIXES = new TreeMap<>();
    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.#", new DecimalFormatSymbols(Locale.ENGLISH));
    private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("#,##0", new DecimalFormatSymbols(Locale.ENGLISH));

    static {
        NUMBER_SUFFIXES.put(1_000d, 'k');
        NUMBER_SUFFIXES.put(1_000_000d, 'm');
        NUMBER_SUFFIXES.put(1_000_000_000d, 'b');
        NUMBER_SUFFIXES.put(1_000_000_000_000d, 't');
    }

    private Utils() {
    }

    public static boolean isValidUuid(String uuid) {
        return VALID_UUID_PATTERN.matcher(uuid).matches();
    }

    public static boolean isInvalidMcName(String username) {
        return !VALID_USERNAME.matcher(username).matches();
    }

    public static String fancyCase(String string) {
        return WordUtils.capitalizeFully(string.replace('_', ' '), ' ', '-');
    }

    /**
     * Replaces &colorCode with §colorCode
     */
    public static String toMcColorCodes(String string) {
        Matcher matcher = ALTERNATE_COLOR_CODES_PATTERN.matcher(string);
        return matcher.replaceAll("§$1");
    }

    /**
     * Replaces §colorCode with &colorCode
     */
    public static String toHumanColorCodes(String string) {
        Matcher matcher = MC_COLOR_CODES_PATTERN.matcher(string);
        return matcher.replaceAll("&$1");
    }

    /**
     * Turn timestamp into pretty-formatted duration and date details.
     *
     * @param timestamp last login/logout
     * @return 1st: duration between timestamp and now in words; 2nd: formatted date if time differences is >24h, otherwise null
     */
    public static Pair<String, String> getDurationAsWords(long timestamp) {
        long duration = System.currentTimeMillis() - timestamp;
        long daysPast = TimeUnit.MILLISECONDS.toDays(duration);

        String dateFormatted = null;
        if (daysPast > 1) {
            dateFormatted = DateFormatUtils.format(timestamp, "dd-MMM-yyyy");
        }

        if (daysPast > 31) {
            return Pair.of(
                    DurationFormatUtils.formatPeriod(timestamp, System.currentTimeMillis(), (daysPast > 365 ? "y 'years' " : "") + "M 'months' d 'days'"),
                    dateFormatted);
        } else {
            return Pair.of(
                    DurationFormatUtils.formatDurationWords(duration, true, true),
                    dateFormatted);
        }
    }

    public static String getDurationAsWord(long timestamp) {
        long duration = System.currentTimeMillis() - timestamp;
        long secondsPast = TimeUnit.MILLISECONDS.toSeconds(duration);
        if (secondsPast < 60) {
            return secondsPast + " second" + (secondsPast > 1 ? "s" : "");
        }
        long minutesPast = TimeUnit.SECONDS.toMinutes(secondsPast);
        if (minutesPast < 60) {
            return minutesPast + " minute" + (minutesPast > 1 ? "s" : "");
        }
        long hoursPast = TimeUnit.MINUTES.toHours(minutesPast);
        if (hoursPast < 24) {
            return hoursPast + " hour" + (hoursPast > 1 ? "s" : "");
        }
        long daysPast = TimeUnit.HOURS.toDays(hoursPast);
        if (daysPast < 31) {
            return daysPast + " day" + (daysPast > 1 ? "s" : "");
        }
        double monthsPast = daysPast / 30.5d;
        if (monthsPast < 12) {
            return DECIMAL_FORMAT.format(monthsPast) + " month" + (monthsPast >= 2 ? "s" : "");
        }
        double yearsPast = monthsPast / 12d;
        return DECIMAL_FORMAT.format(yearsPast) + " year" + (yearsPast >= 2 ? "s" : "");
    }

    public static String toRealPath(Path path) {
        try {
            return path.toRealPath().toString();
        } catch (IOException e) {
            e.printStackTrace();
            return "file not found";
        }
    }

    public static String toRealPath(File path) {
        return toRealPath(path.toPath());
    }

    public static String formatDecimal(double number) {
        return DECIMAL_FORMAT.format(number);
    }

    public static String formatNumber(double number) {
        return NUMBER_FORMAT.format(number);
    }

    /**
     * Formats a large number with abbreviations for each factor of a thousand (k, m, b, t)
     */
    public static String formatNumberWithAbbreviations(double number) {
        if (number == Double.MIN_VALUE) return formatNumberWithAbbreviations(Double.MIN_VALUE + 1);
        if (number < 0) return "-" + formatNumberWithAbbreviations(-number);
        if (number < 1000) return "" + (long) number;

        Map.Entry<Double, Character> e = NUMBER_SUFFIXES.floorEntry(number);
        Double divideBy = e.getKey();
        Character suffix = e.getValue();

        String amountOfDecimals;
        switch (suffix) {
            case 'k':
                amountOfDecimals = "#";
                break;
            case 'm':
                amountOfDecimals = "##";
                break;
            default:
                amountOfDecimals = "###";
        }
        DecimalFormat df = new DecimalFormat("#,##0." + amountOfDecimals, new DecimalFormatSymbols(Locale.ENGLISH));
        return df.format(number / divideBy) + suffix;
    }

    /**
     * Convert Roman numerals to their corresponding Arabic numeral
     *
     * @param roman Roman numeral
     * @return Arabic numeral
     * @see <a href="https://www.w3resource.com/javascript-exercises/javascript-math-exercise-22.php">Source</a>
     */
    public static int convertRomanToArabic(String roman) {
        if (roman == null) return -1;
        int number = romanCharToArabic(roman.charAt(0));

        for (int i = 1; i < roman.length(); i++) {
            int current = romanCharToArabic(roman.charAt(i));
            int previous = romanCharToArabic(roman.charAt(i - 1));
            if (current <= previous) {
                number += current;
            } else {
                number = number - previous * 2 + current;
            }
        }
        return number;
    }

    private static int romanCharToArabic(char c) {
        switch (c) {
            case 'I':
                return 1;
            case 'V':
                return 5;
            case 'X':
                return 10;
            case 'L':
                return 50;
            case 'C':
                return 100;
            case 'D':
                return 500;
            case 'M':
                return 1000;
            default:
                return -1;
        }
    }

    /**
     * Convert Arabic numerals to their corresponding Roman numerals
     *
     * @param number Arabic numerals
     * @return Roman numerals
     * @see <a href="https://stackoverflow.com/a/48357180">Source</a>
     */
    public static String convertArabicToRoman(int number) {
        if (number == 0) {
            return "0";
        }
        String romanOnes = arabicToRomanChars(number % 10, "I", "V", "X");
        number /= 10;

        String romanTens = arabicToRomanChars(number % 10, "X", "L", "C");
        number /= 10;

        String romanHundreds = arabicToRomanChars(number % 10, "C", "D", "M");
        number /= 10;

        String romanThousands = arabicToRomanChars(number % 10, "M", "", "");

        return romanThousands + romanHundreds + romanTens + romanOnes;
    }

    private static String arabicToRomanChars(int n, String one, String five, String ten) {
        switch (n) {
            case 1:
                return one;
            case 2:
                return one + one;
            case 3:
                return one + one + one;
            case 4:
                return one + five;
            case 5:
                return five;
            case 6:
                return five + one;
            case 7:
                return five + one + one;
            case 8:
                return five + one + one + one;
            case 9:
                return one + ten;
            default:
                return "";
        }
    }

    /**
     * Get the minion tier's color for chat formatting
     *
     * @param tier minion tier
     * @return color code corresponding to the tier
     */
    public static EnumChatFormatting getMinionTierColor(int tier) {
        EnumChatFormatting tierColor;
        switch (tier) {
            case 1:
                tierColor = EnumChatFormatting.WHITE;
                break;
            case 2:
            case 3:
            case 4:
                tierColor = EnumChatFormatting.GREEN;
                break;
            case 5:
            case 6:
            case 7:
                tierColor = EnumChatFormatting.DARK_PURPLE;
                break;
            case 8:
            case 9:
            case 10:
                tierColor = EnumChatFormatting.RED;
                break;
            case 11:
                tierColor = EnumChatFormatting.AQUA;
                break;
            case 12:
                tierColor = EnumChatFormatting.DARK_AQUA;
                break;
            default:
                tierColor = EnumChatFormatting.OBFUSCATED;
        }
        return tierColor;
    }


    public static String booleanToSymbol(boolean value) {
        return value ? EnumChatFormatting.GREEN + "✔" : EnumChatFormatting.RED + "✘";
    }

    public static <V> Map<String, V> getLastNMapEntries(Map<String, V> map, int numberOfElements) {
        if (map == null || map.isEmpty()) {
            return null;
        }
        if (map.size() <= numberOfElements) {
            return map;
        }
        return map.entrySet().stream().sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
                .limit(numberOfElements)
                .sorted(Map.Entry.comparingByKey())
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
    }

    public static Pair<String, String> extractSbItemBaseName(String originalItemName, NBTTagCompound extraAttributes, boolean strikethrough) {
        String reforge = "";
        StringBuilder modifiedItemName = new StringBuilder(originalItemName);
        String grayedOutFormatting = "" + EnumChatFormatting.GRAY + EnumChatFormatting.STRIKETHROUGH;

        if (extraAttributes.hasKey("modifier")) {
            // item has been reforged; re-format item name to exclude reforges
            reforge = StringUtils.capitalize(extraAttributes.getString("modifier"));
            int modifierSuffix = Math.max(reforge.indexOf("_sword"), reforge.indexOf("_bow"));
            if (modifierSuffix != -1) {
                reforge = reforge.substring(0, modifierSuffix);
            }
            int reforgeInItemName = originalItemName.indexOf(reforge);
            String sbId = extraAttributes.getString("id");
            if (reforgeInItemName == -1 && reforge.equals("Light") && sbId.startsWith("HEAVY_")) {
                // special case: heavy armor with light reforge
                reforgeInItemName = originalItemName.indexOf("Heavy");
            } else if (reforgeInItemName > 10 && reforge.equals("Heavy") && sbId.startsWith("SUPER_HEAVY_")) {
                // special case: super heavy armor with heavy reforge
                reforgeInItemName = originalItemName.indexOf("Super Heavy");
            }

            if (reforgeInItemName > 0 && !originalItemName.contains(EnumChatFormatting.STRIKETHROUGH.toString())) {
                // we have a reforged item! remove reforge in item name and remove any essence upgrades (✪)

                int reforgeLength = reforge.length();
                String reforgePrefix = null;
                // special cases for reforge + item name
                if (reforge.equals("Heavy") && sbId.startsWith("HEAVY_")) {
                    reforgePrefix = "Extremely ";
                } else if (reforge.equals("Light") && sbId.startsWith("HEAVY_")) {
                    reforgePrefix = "Not So ";
                } else if (reforge.equals("Heavy") && sbId.startsWith("SUPER_HEAVY_")) {
                    reforgePrefix = "Thicc ";
                } else if ((reforge.equals("Wise") && sbId.startsWith("WISE_DRAGON_"))
                        || (reforge.equals("Strong") && sbId.startsWith("STRONG_DRAGON_"))) {
                    reforgePrefix = "Very ";
                } else if (reforge.equals("Superior") && sbId.startsWith("SUPERIOR_DRAGON_")) {
                    reforgePrefix = "Highly ";
                } else if (reforge.equals("Perfect") && sbId.startsWith("PERFECT_")) {
                    reforgePrefix = "Absolutely ";
                }
                if (reforgePrefix != null) {
                    reforgeInItemName -= reforgePrefix.length();
                    reforgeLength = reforgePrefix.length() - 1;
                }

                if (strikethrough) {
                    modifiedItemName.insert(reforgeInItemName, grayedOutFormatting)
                            .insert(reforgeInItemName + reforgeLength + grayedOutFormatting.length(), originalItemName.substring(0, reforgeInItemName));
                } else {
                    modifiedItemName.delete(reforgeInItemName, reforgeInItemName + reforgeLength);
                }
            }
        }
        // remove or 'hide' essence upgrade indicators (✪)
        replaceInStringBuilder(modifiedItemName, EnumChatFormatting.GOLD + "✪", grayedOutFormatting + "✪", strikethrough);
        replaceInStringBuilder(modifiedItemName, EnumChatFormatting.RED + "✪", "" + EnumChatFormatting.DARK_GRAY + EnumChatFormatting.STRIKETHROUGH + "✪", strikethrough);

        return Pair.of(modifiedItemName.toString().trim(), reforge);
    }

    private static void replaceInStringBuilder(StringBuilder sb, String search, String replacement, boolean replace) {
        int hit = sb.indexOf(search);
        while (hit > 0) {
            if (replace) {
                sb.replace(hit, hit + search.length(), replacement);
            } else {
                sb.delete(hit, hit + search.length());
            }
            hit = sb.indexOf(search);
        }
    }

    public static void copyToClipboardOrSaveAsFile(String what, String fileName, NBTBase data, boolean sortData) {
        String nbt = GsonUtils.toJson(data, sortData);
        if (MooConfig.copyWailaAndInventoryDataToClipboard()) {
            GuiScreen.setClipboardString(nbt);
            Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Copied " + what + " to clipboard.");
        } else {
            try {
                File target = getTimestampedFileForDirectory(fileName, "json");
                if (target == null) {
                    return;
                }
                Files.write(target.toPath(), nbt.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW);

                File targetNormalized = target.getCanonicalFile();

                Cowlection.getInstance().getChatHelper().sendMessage(new MooChatComponent("Saved " + what).green()
                        .appendSibling(new MooChatComponent(" [open file]").gold().setOpenFile(targetNormalized))
                        .appendSibling(new MooChatComponent(" [open folder]").darkAqua().setOpenFile(targetNormalized.getParentFile())));
            } catch (IOException | UnsupportedOperationException e) {
                e.printStackTrace();
                Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.RED, "Couldn't save " + what + ": " + e.toString());
            }
        }
    }

    /**
     * Based on ScreenShotHelper#getTimestampedPNGFileForDirectory
     */
    static File getTimestampedFileForDirectory(String suffix, String fileType) {
        File cowlectionOutPath = Cowlection.getInstance().getModOutDirectory();
        if (cowlectionOutPath == null) {
            // dir didn't exist and couldn't be created
            return null;
        }

        String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"));
        String sanitizedSuffix = StringUtils.replaceEach(EnumChatFormatting.getTextWithoutFormattingCodes(suffix).trim(),
                // replacement characters from https://stackoverflow.com/a/61448658
                new String[]{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"},
                new String[]{"⧵", "∕", "∶", "∗", "?", "“", "‹", "›", "∣"});
        String baseName = currentDateTime + "_" + sanitizedSuffix;
        int i = 1;
        while (true) {
            File timestampedFile = new File(cowlectionOutPath, baseName + (i == 1 ? "" : "_" + i) + "." + fileType);
            if (!timestampedFile.exists()) {
                return timestampedFile;
            }
            ++i;
        }
    }
}