001package net.filebot.format;
002
003import static java.util.Arrays.*;
004import static java.util.regex.Pattern.*;
005import static net.filebot.format.ExpressionFormatFunctions.*;
006import static net.filebot.similarity.ICU.*;
007import static net.filebot.similarity.Normalization.*;
008import static net.filebot.util.RegularExpressions.*;
009
010import java.io.File;
011import java.math.BigDecimal;
012import java.time.Instant;
013import java.time.LocalDateTime;
014import java.time.ZoneId;
015import java.time.temporal.Temporal;
016import java.time.temporal.TemporalAmount;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.Date;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Map.Entry;
024import java.util.Objects;
025import java.util.TreeMap;
026import java.util.function.Function;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029import java.util.stream.Collectors;
030import java.util.stream.IntStream;
031import java.util.stream.Stream;
032
033import org.codehaus.groovy.runtime.DefaultGroovyMethods;
034import org.codehaus.groovy.runtime.StringGroovyMethods;
035import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
036
037import groovy.lang.Closure;
038
039import net.filebot.InvalidInputException;
040import net.filebot.media.MediaDetection;
041import net.filebot.util.DateTimeUtilities;
042import net.filebot.util.FileUtilities;
043import net.filebot.util.StringUtilities;
044
045public class ExpressionFormatMethods {
046
047        /**
048         * Convert all characters to lower case/
049         *
050         * e.g. "Firelfy" ➔ "firefly"
051         */
052        public static String lower(String self) {
053                return self.toLowerCase();
054        }
055
056        public static String lower(String self, String pattern) {
057                return StringUtilities.replaceAll(self, compile(pattern), (i, s) -> lower(s));
058        }
059
060        /**
061         * Convert all characters to upper case.
062         *
063         * e.g. "Firelfy" ➔ "FIREFLY"
064         */
065        public static String upper(String self) {
066                return self.toUpperCase();
067        }
068
069        public static String upper(String self, String pattern) {
070                return StringUtilities.replaceAll(self, compile(pattern), (i, s) -> upper(s));
071        }
072
073        /**
074         * Pad number patterns to length using the given character.
075         *
076         * e.g. "1x01" ➔ "01x001"
077         */
078        public static String pad(String self, int... length) {
079                if (length == null || length.length == 0) {
080                        throw new InvalidInputException("1 padding length is required");
081                }
082
083                return StringUtilities.replaceAll(self, DIGIT, (i, m) -> {
084                        return pad(m, length[i < length.length ? i : length.length - 1], "0");
085                });
086        }
087
088        public static String pad(Number self, int length) {
089                return pad(self.toString(), length, "0");
090        }
091
092        public static String pad(CharSequence self, int length, CharSequence padding) {
093                return StringGroovyMethods.padLeft(self, length, padding);
094        }
095
096        /**
097         * Round decimal number to precision.
098         *
099         * e.g. "3.14" ➔ "3.1"
100         */
101        public static Number round(Number self, int precision) {
102                return DefaultGroovyMethods.round(BigDecimal.valueOf(self.doubleValue()), precision);
103        }
104
105        /**
106         * Match pattern and return or unwind if pattern cannot be found.
107         */
108        public static String match(String self, String pattern) {
109                return match(self, pattern, -1);
110        }
111
112        public static String match(String self, String pattern, int matchGroup) {
113                Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE).matcher(self);
114                if (matcher.find()) {
115                        return firstCapturingGroup(matcher, matchGroup);
116                }
117                throw new InvalidInputException("Pattern not found: " + pattern + ": " + self);
118        }
119
120        /**
121         * Match all occurrences of the given pattern or unwind if pattern cannot be found.
122         */
123        public static List<String> matchAll(String self, String pattern) {
124                return matchAll(self, pattern, -1);
125        }
126
127        public static List<String> matchAll(String self, String pattern, int matchGroup) {
128                List<String> matches = new ArrayList<String>();
129                Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE).matcher(self);
130                while (matcher.find()) {
131                        matches.add(firstCapturingGroup(matcher, matchGroup));
132                }
133
134                if (matches.isEmpty()) {
135                        throw new InvalidInputException("Pattern not found: " + pattern + ": " + self);
136                }
137                return matches;
138        }
139
140        private static String firstCapturingGroup(Matcher self, int matchGroup) {
141                int g = matchGroup < 0 ? self.groupCount() > 0 ? 1 : 0 : matchGroup;
142
143                // return the entire match
144                if (g == 0) {
145                        return self.group();
146                }
147
148                // otherwise find first non-empty capturing group
149                return IntStream.rangeClosed(g, self.groupCount()).mapToObj(self::group).filter(Objects::nonNull).map(String::trim).filter(s -> s.length() > 0).findFirst().orElseThrow(() -> {
150                        return new InvalidInputException("Capturing group " + g + " not found");
151                });
152        }
153
154        public static String replaceAll(String self, String pattern) {
155                return compile(pattern).matcher(self).replaceAll("");
156        }
157
158        public static String removeAll(String self, String pattern) {
159                return compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE).matcher(self).replaceAll("").trim();
160        }
161
162        public static String remove(String self, String character, String... characters) {
163                return Stream.of(characters).reduce(self.replace(character, ""), (s, c) -> s.replace(c, ""));
164        }
165
166        /**
167         * Strip characters that aren't allowed on Windows from the given filename.
168         *
169         * e.g. "Sissi: The Young Empress" ➔ "Sissi The Young Empress"
170         */
171        public static String removeIllegalCharacters(String self) {
172                return FileUtilities.validateFileName(normalizeQuotationMarks(self));
173        }
174
175        /**
176         * Replace characters that aren't allowed on Windows from the given filename with similar-looking unicode equivalents
177         */
178        public static String replaceIllegalCharacters(String self) {
179                return org.codehaus.groovy.util.StringUtil.tr(self, "<>:\"/|?*\\", "﹤﹥꞉“⁄⼁?﹡∖");
180        }
181
182        /**
183         * Strip brackets and other clutter patterns.
184         * 
185         * e.g. [ONe]_Ano_Hana_01_(1280x720) ➔ Ano Hana 01
186         */
187        public static String clean(String self) {
188                return FileUtilities.validateFileName(MediaDetection.stripReleaseInfo(self, true));
189        }
190
191        /**
192         * Replace all spaces.
193         *
194         * e.g. "Doctor Who" ➔ "Doctor_Who"
195         */
196        public static String space(String self, String replacement) {
197                return normalizeSpace(self, replacement);
198        }
199
200        /**
201         * Replace all colons.
202         *
203         * e.g. "Sissi: The Young Empress" ➔ "Sissi - The Young Empress"
204         */
205        public static String colon(String self, String colon) {
206                return COLON.matcher(self).replaceAll(colon);
207        }
208
209        public static String colon(String self, String colon, String ratio) {
210                return COLON.matcher(RATIO.matcher(self).replaceAll(ratio)).replaceAll(colon);
211        }
212
213        /**
214         * Replace all slashes.
215         *
216         * e.g. "V_MPEG4/ISO/AVC" ➔ "V_MPEG4.ISO.AVC"
217         */
218        public static String slash(String self, String replacement) {
219                return SLASH.matcher(self).replaceAll(replacement);
220        }
221
222        /**
223         * Convert all initial characters to upper case.
224         *
225         * e.g. "The Day a new Demon was born" ➔ "The Day A New Demon Was Born"
226         */
227        public static String upperInitial(String self) {
228                return replaceHeadTail(self, String::toUpperCase, String::toString);
229        }
230
231        /**
232         * Convert all trailing characters to lower case.
233         *
234         * e.g. "Gundam SEED" ➔ "Gundam Seed"
235         */
236        public static String lowerTrail(String self) {
237                return replaceHeadTail(self, String::toString, String::toLowerCase);
238        }
239
240        private static String replaceHeadTail(String self, Function<String, String> head, Function<String, String> tail) {
241                // EXCEPT: I'm, It's, We're, I'll, etc
242                Matcher matcher = compile("\\b((?<!['`´])\\p{Alnum}|['`´](?!m|s|re|ll)\\p{Alnum}(?=\\p{Alnum}))(\\p{Alnum}*)\\b", UNICODE_CHARACTER_CLASS).matcher(self);
243
244                StringBuffer buffer = new StringBuffer();
245                while (matcher.find()) {
246                        matcher.appendReplacement(buffer, head.apply(matcher.group(1)) + tail.apply(matcher.group(2)));
247                }
248
249                return matcher.appendTail(buffer).toString();
250        }
251
252        /**
253         * Convert to sort name.
254         *
255         * e.g. "The Walking Dead" ➔ "Walking Dead"
256         */
257        public static String sortName(String self) {
258                return sortName(self, "$2");
259        }
260
261        public static String sortName(String self, String replacement) {
262                return compile("^(The|A|An)\\s(.+)", CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self).replaceFirst(replacement).trim();
263        }
264
265        public static String sortInitial(String self) {
266                // use primary initial, ignore The XY, A XY, etc
267                return sortName(self).codePoints().mapToObj(c -> {
268                        if (Character.isDigit(c)) {
269                                return "0-9";
270                        }
271                        if (Character.isLetter(c)) {
272                                String character = new StringBuilder(2).appendCodePoint(c).toString();
273                                return ascii(character).substring(0, 1).toUpperCase();
274                        }
275                        return null;
276                }).filter(Objects::nonNull).findFirst().get();
277        }
278
279        /**
280         * Reduce first name to initials.
281         *
282         * e.g. "James Cameron" ➔ "J. Cameron"
283         */
284        public static String initialName(String self) {
285                String[] words = SPACE.split(self);
286                for (int i = 0; i < words.length - 1; i++) {
287                        words[i] = words[i].charAt(0) + ".";
288                }
289                return String.join(" ", words);
290        }
291
292        public static String truncate(String self, int limit) {
293                if (limit >= self.length())
294                        return self;
295
296                return self.substring(0, limit);
297        }
298
299        public static String truncate(String self, int hardLimit, String nonWordPattern) {
300                if (hardLimit >= self.length())
301                        return self;
302
303                int softLimit = 0;
304                Matcher matcher = compile(nonWordPattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
305                while (matcher.find()) {
306                        if (matcher.start() > hardLimit) {
307                                break;
308                        }
309                        softLimit = matcher.start();
310                }
311                return truncate(self, softLimit);
312        }
313
314        /**
315         * Match substring before the given pattern or return the original value.
316         */
317        public static String before(String self, String pattern) {
318                Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
319
320                // pattern was found, return leading substring, else return original value
321                return matcher.find() ? self.substring(0, matcher.start()).trim() : self;
322        }
323
324        /**
325         * Match substring before the given pattern or return the original value.
326         */
327        public static String after(String self, String pattern) {
328                Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
329
330                // pattern was found, return trailing substring, else return original value
331                return matcher.find() ? self.substring(matcher.end(), self.length()).trim() : self;
332        }
333
334        /**
335         * Find match in case-insensitive mode.
336         */
337        public static Matcher findMatch(String self, String pattern) {
338                if (pattern == null || pattern.isEmpty()) {
339                        return null;
340                }
341
342                return compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
343        }
344
345        /**
346         * Find match in between word boundaries in case-insensitive mode.
347         */
348        public static Matcher findWordMatch(String self, String pattern) {
349                if (pattern == null || pattern.isEmpty()) {
350                        return null;
351                }
352
353                return findMatch(self, "(?<!\\p{Alnum})(?:" + pattern + ")(?!\\p{Alnum})");
354        }
355
356        /**
357         * Match brackets pattern.
358         *
359         * e.g. "The IT Crowd (UK)" ➔ "UK"
360         */
361        public static List<String> matchBrackets(String self) {
362                return matchAll(self, "[\\(\\[\\{]([^\\(\\[\\{\\)\\]\\}]+)[\\)\\]\\}]", 1);
363        }
364
365        /**
366         * Remove brackets pattern.
367         *
368         * e.g. "The IT Crowd (UK)" ➔ "The IT Crowd"
369         */
370        public static String removeBrackets(String self) {
371                return removeAll(self, "[\\(\\[\\{]([^\\(\\[\\{\\)\\]\\}]*)[\\)\\]\\}]");
372        }
373
374        /**
375         * Strip trailing parenthesis.
376         *
377         * e.g. "The IT Crowd (UK)" ➔ "The IT Crowd"
378         */
379        public static String replaceTrailingBrackets(String self) {
380                return replaceTrailingBrackets(self, "");
381        }
382
383        /**
384         * Replace trailing parenthesis.
385         *
386         * e.g. "The IT Crowd (UK)" ➔ "The IT Crowd [UK]"
387         */
388        public static String replaceTrailingBrackets(String self, String replacement) {
389                return compile("\\s*[(]([^)]*)[)]$", UNICODE_CHARACTER_CLASS).matcher(self).replaceAll(replacement);
390        }
391
392        /**
393         * Strip trailing part number.
394         *
395         * e.g. "Today Is the Day (1)" ➔ "Today Is the Day"
396         */
397        public static String replacePart(String self) {
398                return replacePart(self, "");
399        }
400
401        /**
402         * Replace trailing part number.
403         *
404         * e.g. "Today Is the Day (1)" ➔ "Today Is the Day, Part 1"
405         */
406        public static String replacePart(String self, String replacement) {
407                // handle '(n)', '(Part n)' and ': Part n' like syntax
408                String[] patterns = new String[] { "\\s*[(](\\w{1,3})[)]$", "\\W+Part (\\w+)\\W*$" };
409
410                for (String pattern : patterns) {
411                        Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS).matcher(self);
412                        if (matcher.find()) {
413                                return matcher.replaceAll(replacement).trim();
414                        }
415                }
416
417                // no pattern matches, nothing to replace
418                return self;
419        }
420
421        /**
422         * Convert to acronym.
423         *
424         * e.g. "Deep Space 9" ➔ "DS9"
425         */
426        public static String acronym(String self) {
427                return compile("\\s|\\B\\p{Alnum}+", UNICODE_CHARACTER_CLASS).matcher(space(self, " ")).replaceAll("");
428        }
429
430        /**
431         * Replace numbers 1..12 with Roman numerals.
432         *
433         * e.g. "Star Wars: Episode 4" ➔ "Star Wars: Episode IV"
434         */
435        public static String roman(String self) {
436                TreeMap<Integer, String> numerals = new TreeMap<Integer, String>();
437                numerals.put(10, "X");
438                numerals.put(9, "IX");
439                numerals.put(5, "V");
440                numerals.put(4, "IV");
441                numerals.put(1, "I");
442
443                StringBuffer s = new StringBuffer();
444                Matcher m = compile("\\b\\d+\\b").matcher(self);
445                while (m.find()) {
446                        int n = Integer.parseInt(m.group());
447                        m.appendReplacement(s, n >= 1 && n <= 12 ? roman(n, numerals) : m.group());
448                }
449                return m.appendTail(s).toString();
450        }
451
452        public static String roman(Integer n, TreeMap<Integer, String> numerals) {
453                int l = numerals.floorKey(n);
454                if (n == l) {
455                        return numerals.get(n);
456                }
457                return numerals.get(l) + roman(n - l, numerals);
458        }
459
460        /**
461         * Apply any ICU script transliteration.
462         *
463         * e.g. "中国" ➔ "zhōng guó"
464         * 
465         * @see http://userguide.icu-project.org/transforms/general
466         */
467        public static String transliterate(String self, String transformIdentifier) {
468                return getTransliterator(transformIdentifier).transform(self);
469        }
470
471        /**
472         * Convert Unicode characters to ASCII.
473         *
474         * e.g. "カタカナ" ➔ "katakana"
475         */
476        public static String ascii(String self) {
477                return ascii(self, " ");
478        }
479
480        public static String ascii(String self, String fallback) {
481                return ASCII.transform(asciiQuotes(self)).replaceAll("\\P{ASCII}+", fallback).trim();
482        }
483
484        public static String asciiQuotes(String self) {
485                return normalizeQuotationMarks(self);
486        }
487
488        public static boolean isLatin(String self) {
489                return LATIN.matcher(self).matches();
490        }
491
492        public static List<String> getGraphemeClusters(String self) {
493                return tokenizeGraphemeClusters(self);
494        }
495
496        /**
497         * Apply replacement mappings.
498         *
499         * e.g. replace(ä:'ae', ö:'oe', ü:'ue')
500         */
501        public static String replace(String self, Map<?, ?> replacer) {
502                for (Entry<?, ?> m : replacer.entrySet()) {
503                        for (Object key : DefaultGroovyMethods.flatten(asList(m.getKey()))) {
504                                Pattern pattern = key instanceof Pattern ? (Pattern) key : compile(quote(key.toString()));
505                                if (m.getValue() instanceof Closure) {
506                                        self = StringGroovyMethods.replaceAll(self, pattern, (Closure) m.getValue());
507                                } else {
508                                        self = StringGroovyMethods.replaceAll(self, pattern, m.getValue().toString());
509                                }
510                        }
511                }
512                return self;
513        }
514
515        /**
516         * Apply replacement mappings.
517         *
518         * e.g. replace('Directors Cut':'DC')
519         */
520        public static Collection<String> replace(Collection<?> self, Map<String, String> replacer) {
521                return self.stream().map(Objects::toString).map(v -> replacer.getOrDefault(v, v)).filter(Objects::nonNull).collect(Collectors.toList());
522        }
523
524        /**
525         * Find matching pattern and return mapped value.
526         * 
527         * e.g. az.match('[a-f]': '/volume1', '[g-x]': '/volume2') ?: '/volume3'
528         */
529        public static Object match(String self, Map<?, ?> matcher) {
530                for (Entry<?, ?> m : matcher.entrySet()) {
531                        Pattern p = m.getKey() instanceof Pattern ? (Pattern) m.getKey() : compile(m.getKey().toString(), CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE);
532                        if (p.matcher(self).find()) {
533                                return m.getValue();
534                        }
535                }
536                return null;
537        }
538
539        public static Object match(Collection<?> self, Map<?, ?> matcher) {
540                for (Entry<?, ?> m : matcher.entrySet()) {
541                        Pattern p = m.getKey() instanceof Pattern ? (Pattern) m.getKey() : compile(m.getKey().toString(), CASE_INSENSITIVE | UNICODE_CHARACTER_CLASS | MULTILINE);
542                        for (Object o : self) {
543                                if (o != null) {
544                                        if (p.matcher(o.toString()).find()) {
545                                                return m.getValue();
546                                        }
547                                }
548                        }
549                }
550                return null;
551        }
552
553        public static String joining(Collection<?> self, String delimiter) {
554                return joining(self, delimiter, "", "");
555        }
556
557        public static String joining(Collection<?> self, String delimiter, String prefix, String suffix) {
558                String[] list = DefaultGroovyMethods.flatten(self).stream().filter(Objects::nonNull).map(Objects::toString).filter(s -> !s.isEmpty()).toArray(String[]::new);
559
560                if (list.length > 0) {
561                        return prefix + String.join(delimiter, list) + suffix;
562                }
563
564                throw new InvalidInputException("Collection did not yield any values: " + self);
565        }
566
567        public static String joiningDistinct(Collection<?> self, String delimiter, Closure<?>... mapper) {
568                return joiningDistinct(self, delimiter, "", "", mapper);
569        }
570
571        public static String joiningDistinct(Collection<?> self, String delimiter, String prefix, String suffix, Closure<?>... mapper) {
572                Stream<?> stream = DefaultGroovyMethods.flatten(self).stream().filter(Objects::nonNull);
573
574                // apply custom mappers if any
575                if (mapper.length > 0) {
576                        stream = stream.flatMap(v -> stream(mapper).map(m -> m.call(v)).filter(Objects::nonNull));
577                }
578
579                // sort unique
580                String[] list = stream.map(Objects::toString).filter(s -> !s.isEmpty()).distinct().toArray(String[]::new);
581                if (list.length > 0) {
582                        return prefix + String.join(delimiter, list) + suffix;
583                }
584
585                throw new InvalidInputException("Collection did not yield any values: " + self);
586        }
587
588        public static List<?> bounds(Iterable<?> self) {
589                return Stream.of(DefaultGroovyMethods.min(self), DefaultGroovyMethods.max(self)).filter(Objects::nonNull).distinct().collect(Collectors.toList());
590        }
591
592        /**
593         * Unwind if an object does not satisfy the given predicate.
594         *
595         * e.g. (0..9)*.check{it < 10}.sum()
596         */
597        public static Object check(Object self, Closure<?> c) {
598                if (DefaultTypeTransformation.castToBoolean(c.call(self))) {
599                        return self;
600                }
601                throw new InvalidInputException("Object failed check: " + self);
602        }
603
604        /**
605         * Add values to the file name, after the file name, but before the subtitle language suffix and file extension.
606         *
607         * e.g. "Avatar (2009).mp4" ➔ "Avatar (2009) [720p].mp4"
608         */
609        public static StructuredFile derive(StructuredFile self, Object tag, Object... tags) {
610                return self.tag(component(null, tag, tags));
611        }
612
613        /**
614         * Add a value to the movie / series folder name.
615         * 
616         * e.g. Avatar (2009)/Avatar (2009) ➔ Avatar (2009) [TMDB-19995]/Avatar (2009)
617         */
618        public static StructuredFile deriveFolder(StructuredFile self, Object tag, Object... tags) {
619                return self.collection(component(null, tag, tags));
620        }
621
622        /**
623         * Add values to the file name, after the file name, but before the subtitle language suffix and file extension.
624         * 
625         * e.g. plex % {" by $director"} % {" [$vf, $vc, $ac]"}
626         */
627        public static StructuredFile mod(StructuredFile self, Object tag) {
628                return derive(self, tag);
629        }
630
631        /**
632         * Add a value to the movie / series folder name.
633         * 
634         * e.g. plex * " [TMDB-$id]"
635         */
636        public static StructuredFile multiply(StructuredFile self, Object tag) {
637                return deriveFolder(self, tag);
638        }
639
640        /**
641         * Add a value to the file name and the movie / series folder name.
642         * 
643         * e.g. plex ** " [TMDB-$id]"
644         */
645        public static StructuredFile power(StructuredFile self, Object tag) {
646                return deriveFolder(derive(self, tag), tag);
647        }
648
649        /**
650         * Nullify the Movies / TV Series folder level.
651         * 
652         * e.g. ~plex
653         */
654        public static StructuredFile bitwiseNegate(StructuredFile self) {
655                return self.category(null);
656        }
657
658        /**
659         * Add a value to the season folder level.
660         * 
661         * e.g. plex << { sy.bounds().joining('-', ' [', ']') }
662         */
663        public static StructuredFile leftShift(StructuredFile self, Object tag) {
664                return self.group(component(null, tag));
665        }
666
667        /**
668         * Add a value to the A-Z folder level.
669         * 
670         * e.g. plex >> { az }
671         */
672        public static StructuredFile rightShift(StructuredFile self, Object tag) {
673                return self.library(component(null, tag));
674        }
675
676        /**
677         * Replace subtitle language suffix.
678         * 
679         * e.g. plex ^ {'.'+lang.ISO2}
680         */
681        public static StructuredFile xor(StructuredFile self, Object tag) {
682                return self.suffix(null).suffix(component(null, tag));
683        }
684
685        /*
686         * File extensions
687         */
688
689        public static File ascii(File self) {
690                return SLASH.splitAsStream(self.getPath()).map(s -> {
691                        // Any-ASCII may introduce / into the file path (e.g. ½ => 1/2)
692                        return slash(ascii(s), " ");
693                }).collect(Collectors.collectingAndThen(Collectors.joining(File.separator), File::new));
694        }
695
696        public static long getDiskSpace(File self) {
697                return FileUtilities.getDiskSpace(self);
698        }
699
700        public static File getRoot(File self) {
701                return FileUtilities.listPath(self).get(0);
702        }
703
704        public static File getTail(File self) {
705                return FileUtilities.getRelativePathTail(self, FileUtilities.listPath(self).size() - 1);
706        }
707
708        public static List<File> listPath(File self) {
709                return FileUtilities.listPath(self);
710        }
711
712        public static List<File> listPath(File self, int tailSize) {
713                return FileUtilities.listPath(FileUtilities.getRelativePathTail(self, tailSize));
714        }
715
716        public static File getRelativePathTail(File self, int tailSize) {
717                return FileUtilities.getRelativePathTail(self, tailSize);
718        }
719
720        public static LocalDateTime toDate(Long self) {
721                return LocalDateTime.ofInstant(Instant.ofEpochMilli(self), ZoneId.systemDefault());
722        }
723
724        public static LocalDateTime toDate(Long self, String zone) {
725                return LocalDateTime.ofInstant(Instant.ofEpochMilli(self), ZoneId.of(zone, ZoneId.SHORT_IDS));
726        }
727
728        public static File toFile(String self) {
729                if (self == null || self.isEmpty()) {
730                        return null;
731                }
732                return new File(self);
733        }
734
735        public static File toFile(String self, String parent) {
736                if (self == null || self.isEmpty()) {
737                        return null;
738                }
739                File file = new File(self);
740                if (file.isAbsolute()) {
741                        return file;
742                }
743                return new File(parent, self);
744        }
745
746        public static Locale toLocale(String self) {
747                return Locale.forLanguageTag(self);
748        }
749
750        /*
751         * Date extensions
752         */
753
754        public static String format(Temporal self, String pattern) {
755                return format(self, pattern, Locale.US);
756        }
757
758        public static String format(Temporal self, String pattern, Locale locale) {
759                return DateTimeUtilities.format(self, pattern, locale);
760        }
761
762        public static String format(TemporalAmount self, String pattern) {
763                return format(self, pattern, Locale.US);
764        }
765
766        public static String format(TemporalAmount self, String pattern, Locale locale) {
767                return DateTimeUtilities.format(self, pattern, locale);
768        }
769
770        public static String format(Date self, String format) {
771                return format(self, format, Locale.US);
772        }
773
774        public static String format(Date self, String format, Locale locale) {
775                return DateTimeUtilities.format(self, format, locale);
776        }
777
778        public static Date parseDate(String self, String format) {
779                return parseDate(self, format, Locale.US);
780        }
781
782        public static Date parseDate(String self, String format, Locale locale) {
783                return DateTimeUtilities.toDate(DateTimeUtilities.parseDateTime(self, format, locale, ZoneId.systemDefault()));
784        }
785
786        public static Date parseDate(String self) {
787                return DateTimeUtilities.toDate(DateTimeUtilities.parseDateTime(self, ZoneId.systemDefault()));
788        }
789
790        public static int compareTo(Temporal self, String date) {
791                return DateTimeUtilities.toDateTime(self).toInstant().compareTo(DateTimeUtilities.parseDateTime(date, ZoneId.systemDefault()));
792        }
793
794        /*
795         * DSL extensions
796         */
797
798        public static File plus(File self, String path) {
799                return new File(self.getPath().concat(path));
800        }
801
802        public static File div(File self, String path) {
803                return new File(self, path);
804        }
805
806        public static File div(String self, String path) {
807                return new File(self, path);
808        }
809
810        public static File div(File self, File path) {
811                return new File(self, path.getPath());
812        }
813
814        public static File div(String self, File path) {
815                return new File(self, path.getPath());
816        }
817
818        public static File mod(File self, Object suffix) {
819                String name = FileUtilities.getNameWithoutExtension(self.getName());
820                String extension = self.getName().substring(name.length());
821
822                return new File(self.getParentFile(), component(null, name, suffix, extension));
823        }
824
825        public static String plus(String self, Closure closure) {
826                return concat(null, self, closure);
827        }
828
829        public static ReverseComparable negative(Comparable self) {
830                return new ReverseComparable(self);
831        }
832
833        private ExpressionFormatMethods() {
834                throw new UnsupportedOperationException();
835        }
836
837}