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