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}