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