From eb0b999f4e993bd1697a691127e15f25f2d99aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Tue, 10 Sep 2019 14:52:43 +0200 Subject: [PATCH] add cron expressions, update to OpenJDK 11, gradle 5.6 --- CREDITS.txt | 4 + build.gradle | 144 +++-- gradle.properties | 7 +- gradle/ext.gradle | 8 - gradle/publish.gradle | 63 --- gradle/sonarqube.gradle | 36 -- gradle/wrapper/gradle-wrapper.jar | Bin 54227 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 6 +- gradlew | 28 +- gradlew.bat | 18 +- .../java/org/xbib/time/chronic/Chronic.java | 26 +- .../time/chronic/repeaters/RepeaterUnit.java | 4 +- .../xbib/time/schedule/CronExpression.java | 130 +++++ .../org/xbib/time/schedule/CronSchedule.java | 82 +++ .../xbib/time/schedule/DayOfMonthField.java | 122 +++++ .../xbib/time/schedule/DayOfWeekField.java | 173 ++++++ .../time/schedule/DefaultCronExpression.java | 228 ++++++++ .../org/xbib/time/schedule/DefaultField.java | 177 ++++++ .../java/org/xbib/time/schedule/Entry.java | 57 ++ .../java/org/xbib/time/schedule/Keywords.java | 42 ++ .../org/xbib/time/schedule/MatchAllField.java | 23 + .../org/xbib/time/schedule/MonthField.java | 48 ++ .../time/schedule/RebootCronExpression.java | 26 + .../org/xbib/time/schedule/TimeField.java | 12 + .../java/org/xbib/time/schedule/Token.java | 15 + .../java/org/xbib/time/schedule/Tokens.java | 189 +++++++ .../org/xbib/time/schedule/package-info.java | 4 + .../org/xbib/time/util/AbstractMultiMap.java | 126 +++++ .../xbib/time/util/LinkedHashSetMultiMap.java | 29 + .../java/org/xbib/time/util/MultiMap.java | 38 ++ .../java/org/xbib/time/util/TreeMultiMap.java | 31 ++ .../java/org/xbib/time/util/package-info.java | 4 + ...erTest.java => DateTimeFormatterTest.java} | 2 +- .../pretty/PrettyTimeAPIManipulationTest.java | 6 +- .../time/pretty/PrettyTimeI18n_AR_Test.java | 502 +++++++++--------- .../time/pretty/PrettyTimeI18n_BG_Test.java | 448 ++++++++-------- .../time/pretty/PrettyTimeI18n_CA_Test.java | 22 +- .../time/pretty/PrettyTimeI18n_CS_Test.java | 25 +- .../time/pretty/PrettyTimeI18n_DA_Test.java | 321 +++++------ .../time/pretty/PrettyTimeI18n_ET_Test.java | 10 + .../time/pretty/PrettyTimeI18n_FA_Test.java | 477 +++++++++-------- .../time/pretty/PrettyTimeI18n_FI_Test.java | 11 + .../time/pretty/PrettyTimeI18n_FR_Test.java | 22 +- .../time/pretty/PrettyTimeI18n_IT_Test.java | 11 + .../time/pretty/PrettyTimeI18n_KO_Test.java | 12 +- .../time/pretty/PrettyTimeI18n_NL_Test.java | 13 +- .../time/pretty/PrettyTimeI18n_NO_Test.java | 336 ++++++------ .../time/pretty/PrettyTimeI18n_RU_Test.java | 6 +- .../time/pretty/PrettyTimeI18n_SV_Test.java | 10 + .../xbib/time/pretty/PrettyTimeI18n_Test.java | 258 +++++---- .../time/pretty/PrettyTimeI18n_UA_Test.java | 5 +- .../pretty/PrettyTimeI18n_hi_IN_Test.java | 7 +- .../pretty/PrettyTimeI18n_in_ID_Test.java | 5 +- .../pretty/PrettyTimeI18n_zh_TW_Test.java | 7 +- .../pretty/PrettyTimeLocaleFallbackTest.java | 8 +- .../org/xbib/time/pretty/PrettyTimeTest.java | 7 +- .../PrettyTimeUnitConfigurationTest.java | 7 +- .../time/pretty/SimpleTimeFormatTest.java | 6 +- ...impleTimeFormatTimeQuantifiedNameTest.java | 12 +- .../pretty/i18n/TimeFormatProviderTest.java | 5 + .../schedule/CompareBehaviorToQuartzTest.java | 261 +++++++++ .../schedule/CompareSizeToQuartzTest.java | 123 +++++ .../schedule/CompareSpeedToQuartzTest.java | 66 +++ .../time/schedule/CronExpressionTest.java | 344 ++++++++++++ .../xbib/time/schedule/CronScheduleTest.java | 126 +++++ .../org/xbib/time/schedule/DateTimes.java | 65 +++ .../time/schedule/DayOfMonthFieldTest.java | 36 ++ .../time/schedule/DayOfWeekFieldTest.java | 28 + .../xbib/time/schedule/DefaultFieldTest.java | 125 +++++ .../java/org/xbib/time/schedule/Integers.java | 42 ++ .../org/xbib/time/schedule/KeywordsTest.java | 79 +++ .../xbib/time/schedule/MonthFieldTest.java | 27 + .../xbib/time/schedule/NextExecutionTest.java | 79 +++ .../time/schedule/ObjectSizeCalculator.java | 436 +++++++++++++++ .../org/xbib/time/schedule/ReadmeTest.java | 38 ++ .../java/org/xbib/time/schedule/Times.java | 102 ++++ .../org/xbib/time/schedule/TokensTest.java | 204 +++++++ .../schedule/WhatQuartzDoesNotSupport.java | 53 ++ src/test/resources/log4j2.xml | 13 - 79 files changed, 5286 insertions(+), 1422 deletions(-) delete mode 100644 gradle/ext.gradle delete mode 100644 gradle/publish.gradle delete mode 100644 gradle/sonarqube.gradle create mode 100644 src/main/java/org/xbib/time/schedule/CronExpression.java create mode 100644 src/main/java/org/xbib/time/schedule/CronSchedule.java create mode 100644 src/main/java/org/xbib/time/schedule/DayOfMonthField.java create mode 100644 src/main/java/org/xbib/time/schedule/DayOfWeekField.java create mode 100644 src/main/java/org/xbib/time/schedule/DefaultCronExpression.java create mode 100644 src/main/java/org/xbib/time/schedule/DefaultField.java create mode 100644 src/main/java/org/xbib/time/schedule/Entry.java create mode 100644 src/main/java/org/xbib/time/schedule/Keywords.java create mode 100644 src/main/java/org/xbib/time/schedule/MatchAllField.java create mode 100644 src/main/java/org/xbib/time/schedule/MonthField.java create mode 100644 src/main/java/org/xbib/time/schedule/RebootCronExpression.java create mode 100644 src/main/java/org/xbib/time/schedule/TimeField.java create mode 100644 src/main/java/org/xbib/time/schedule/Token.java create mode 100644 src/main/java/org/xbib/time/schedule/Tokens.java create mode 100644 src/main/java/org/xbib/time/schedule/package-info.java create mode 100644 src/main/java/org/xbib/time/util/AbstractMultiMap.java create mode 100644 src/main/java/org/xbib/time/util/LinkedHashSetMultiMap.java create mode 100644 src/main/java/org/xbib/time/util/MultiMap.java create mode 100644 src/main/java/org/xbib/time/util/TreeMultiMap.java create mode 100644 src/main/java/org/xbib/time/util/package-info.java rename src/test/java/org/xbib/time/{FormatterTest.java => DateTimeFormatterTest.java} (95%) create mode 100644 src/test/java/org/xbib/time/schedule/CompareBehaviorToQuartzTest.java create mode 100644 src/test/java/org/xbib/time/schedule/CompareSizeToQuartzTest.java create mode 100644 src/test/java/org/xbib/time/schedule/CompareSpeedToQuartzTest.java create mode 100644 src/test/java/org/xbib/time/schedule/CronExpressionTest.java create mode 100644 src/test/java/org/xbib/time/schedule/CronScheduleTest.java create mode 100644 src/test/java/org/xbib/time/schedule/DateTimes.java create mode 100644 src/test/java/org/xbib/time/schedule/DayOfMonthFieldTest.java create mode 100644 src/test/java/org/xbib/time/schedule/DayOfWeekFieldTest.java create mode 100644 src/test/java/org/xbib/time/schedule/DefaultFieldTest.java create mode 100644 src/test/java/org/xbib/time/schedule/Integers.java create mode 100644 src/test/java/org/xbib/time/schedule/KeywordsTest.java create mode 100644 src/test/java/org/xbib/time/schedule/MonthFieldTest.java create mode 100644 src/test/java/org/xbib/time/schedule/NextExecutionTest.java create mode 100644 src/test/java/org/xbib/time/schedule/ObjectSizeCalculator.java create mode 100644 src/test/java/org/xbib/time/schedule/ReadmeTest.java create mode 100644 src/test/java/org/xbib/time/schedule/Times.java create mode 100644 src/test/java/org/xbib/time/schedule/TokensTest.java create mode 100644 src/test/java/org/xbib/time/schedule/WhatQuartzDoesNotSupport.java delete mode 100644 src/test/resources/log4j2.xml diff --git a/CREDITS.txt b/CREDITS.txt index c42ab7d..cb99950 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -7,6 +7,8 @@ org.xbib.time is based upon the following software: - prettytime https://github.com/ocpsoft/prettytime (Apache 2.0) +- cron expression https://github.com/anderswisch/cron-expression/ (MIT License) + with improvements by Jörg Prante including - converted to Java 8 java.time API @@ -16,3 +18,5 @@ with improvements by Jörg Prante including - refactoring classes - rewritten code to simplify and ease development + +- added nextExecution() method to cron expression and scheduling via Callable diff --git a/build.gradle b/build.gradle index 2b28c4a..3a96d10 100644 --- a/build.gradle +++ b/build.gradle @@ -1,76 +1,134 @@ plugins { - id "org.sonarqube" version "2.2" - id 'org.ajoberstar.github-pages' version '1.6.0-rc.1' - id "org.xbib.gradle.plugin.jbake" version "1.2.1" + id "com.github.spotbugs" version "2.0.0" + id "io.codearte.nexus-staging" version "0.11.0" } -printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + - "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", - InetAddress.getLocalHost(), - System.getProperty("os.name"), - System.getProperty("os.arch"), - System.getProperty("os.version"), - System.getProperty("java.version"), - System.getProperty("java.vm.version"), - System.getProperty("java.vm.vendor"), - System.getProperty("java.vm.name"), - GroovySystem.getVersion(), - gradle.gradleVersion - apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'signing' -apply plugin: 'findbugs' apply plugin: 'pmd' apply plugin: 'checkstyle' -apply plugin: "jacoco" -apply plugin: 'org.ajoberstar.github-pages' - -configurations { - wagon - provided - testCompile.extendsFrom(provided) -} +apply plugin: "com.github.spotbugs" dependencies { - testCompile 'junit:junit:4.12' - testCompile 'org.apache.logging.log4j:log4j-core:2.7' - testCompile 'org.apache.logging.log4j:log4j-jul:2.7' - wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' + testCompile "junit:junit:${project.property('junit.version')}" + testCompile "org.quartz-scheduler:quartz:${project.property('quartz.version')}" + testCompile "com.google.caliper:caliper:${project.property('caliper.version')}" } -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +compileJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all" << "-profile" << "compact2" + options.compilerArgs << "-Xlint:all" } test { testLogging { - showStandardStreams = false + showStandardStreams = true exceptionFormat = 'full' } - systemProperty 'java.util.logging.manager', 'org.apache.logging.log4j.jul.LogManager' +} + +spotbugs { + toolVersion = '3.1.12' + sourceSets = [sourceSets.main] + ignoreFailures = true + effort = "max" + reportLevel = "high" +} + +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} + +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } } task sourcesJar(type: Jar, dependsOn: classes) { classifier 'sources' from sourceSets.main.allSource } + task javadocJar(type: Jar, dependsOn: javadoc) { classifier 'javadoc' } + artifacts { archives sourcesJar, javadocJar } -if (project.hasProperty('signing.keyId')) { - signing { - sign configurations.archives + +ext { + user = 'xbib' + projectName = 'time' + projectDescription = 'A bundle of Chronic, Prettytime, and org.joda.time.format optimized for Java 8 Time API' + scmUrl = 'https://github.com/xbib/time' + scmConnection = 'scm:git:git://github.com/xbib/time.git' + scmDeveloperConnection = 'scm:git:git://github.com/xbib/time.git' +} + +task sonatypeUpload(type: Upload) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('ossrhUsername')) { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: uri(ossrhReleaseUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + snapshotRepository(url: uri(ossrhSnapshotUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + pom.project { + name projectName + description projectDescription + packaging 'jar' + inceptionYear '2016' + url scmUrl + organization { + name 'xbib' + url 'http://xbib.org' + } + developers { + developer { + id user + name 'Jörg Prante' + email 'joergprante@gmail.com' + url 'https://github.com/jprante' + } + } + scm { + url scmUrl + connection scmConnection + developerConnection scmDeveloperConnection + } + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } } } -apply from: 'gradle/ext.gradle' -apply from: 'gradle/publish.gradle' -apply from: 'gradle/sonarqube.gradle' +nexusStaging { + packageGroup = "org.xbib" +} diff --git a/gradle.properties b/gradle.properties index 852f762..ae672f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,8 @@ group = org.xbib name = time -version = 1.0.0 +version = 2.0.0 + +# test +junit.version = 4.12 +quartz.version = 2.3.0 +caliper.version = 1.0-beta-2 diff --git a/gradle/ext.gradle b/gradle/ext.gradle deleted file mode 100644 index 2146626..0000000 --- a/gradle/ext.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - user = 'xbib' - projectName = 'time' - projectDescription = 'A bundle of Chronic, Prettytime, and org.joda.time.format optimized for Java 8 Time API' - scmUrl = 'https://github.com/xbib/time' - scmConnection = 'scm:git:git://github.com/xbib/time.git' - scmDeveloperConnection = 'scm:git:git://github.com/xbib/time.git' -} diff --git a/gradle/publish.gradle b/gradle/publish.gradle deleted file mode 100644 index 59f73fd..0000000 --- a/gradle/publish.gradle +++ /dev/null @@ -1,63 +0,0 @@ - -task xbibUpload(type: Upload) { - configuration = configurations.archives - uploadDescriptor = true - repositories { - if (project.hasProperty('xbibUsername')) { - mavenDeployer { - configuration = configurations.wagon - repository(url: uri('scpexe://xbib.org/repository')) { - authentication(userName: xbibUsername, privateKey: xbibPrivateKey) - } - } - } - } -} - -task sonatypeUpload(type: Upload) { - configuration = configurations.archives - uploadDescriptor = true - repositories { - if (project.hasProperty('ossrhUsername')) { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - repository(url: uri(ossrhReleaseUrl)) { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - snapshotRepository(url: uri(ossrhSnapshotUrl)) { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - pom.project { - name projectName - description projectDescription - packaging 'jar' - inceptionYear '2016' - url scmUrl - organization { - name 'xbib' - url 'http://xbib.org' - } - developers { - developer { - id user - name 'Jörg Prante' - email 'joergprante@gmail.com' - url 'https://github.com/jprante' - } - } - scm { - url scmUrl - connection scmConnection - developerConnection scmDeveloperConnection - } - licenses { - license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - } - } - } - } -} diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle deleted file mode 100644 index b31eafb..0000000 --- a/gradle/sonarqube.gradle +++ /dev/null @@ -1,36 +0,0 @@ -tasks.withType(FindBugs) { - ignoreFailures = true - reports { - xml.enabled = true - } -} -tasks.withType(Pmd) { - ignoreFailures = true - reports { - xml.enabled = true - } -} -tasks.withType(Checkstyle) { - ignoreFailures = true - reports { - xml.enabled = true - } -} - -jacocoTestReport { - reports { - xml.enabled true - xml.destination "${buildDir}/reports/jacoco-xml" - } -} - -sonarqube { - properties { - property "sonar.projectName", "${project.group} ${project.name}" - property "sonar.sourceEncoding", "UTF-8" - property "sonar.tests", "src/test/java" - property "sonar.scm.provider", "git" - property "sonar.java.coveragePlugin", "jacoco" - property "sonar.junit.reportsPath", "build/test-results/test/" - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 51288f9c2f05faf8d42e1a751a387ca7923882c3..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch literal 55616 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∓+@oG%E9CL*K7 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:guess). If no date or time can be found, + * (depending on the value of {@code :guess}). If no date or time can be found, * +nil+ will be returned. *

* Options are: *

- * [:context] - * :past or :future (defaults to :future) + * [{@code :context}] + * {@code :past} or {@code :future} (defaults to {@code :future}) *

- * If your string represents a birthday, you can set :context to :past + * If your string represents a birthday, you can set {@code :context} to {@code :past} * and if an ambiguous string is given, it will assume it is in the - * past. Specify :future or omit to set a future context. + * past. Specify {@code :future<} or omit to set a future context. *

- * [:now] + * [{@code :now}] * Time (defaults to Time.now) *

- * By setting :now to a Time, all computations will be based off + * By setting {@code :now} to a Time, all computations will be based off * of that time instead of Time.now *

- * [:guess] + * [{@code :guess<}] * +true+ or +false+ (defaults to +true+) *

* By default, the parser will guess a single point in time for the * given date or time. If you'd rather have the entire time span returned, - * set :guess to +false+ and a Chronic::Span will be returned. + * set {@code :guess} to +false+ and a Chronic::Span will be returned. *

- * [:ambiguous_time_range] - * Integer or :none (defaults to 6 (6am-6pm)) + * [{@code :ambiguous_time_range}] + * Integer or {@code :none} (defaults to {@code 6} (6am-6pm)) *

* If an Integer is given, ambiguous times (like 5:00) will be * assumed to be within the range of that time in the AM to that time - * in the PM. For example, if you set it to 7, then the parser will + * in the PM. For example, if you set it to {@code 7}, then the parser will * look for the time between 7am and 7pm. In the case of 5:00, it would - * assume that means 5:00pm. If :none is given, no assumption + * assume that means 5:00pm. If {@code :none} is given, no assumption * will be made, and the first matching instance of that time will * be used. * @param text text diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java index dd70bee..d758ce5 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java @@ -43,7 +43,9 @@ public abstract class RepeaterUnit extends Repeater { String unitName = unitNameEnum.name(); String capitalizedUnitName = unitName.substring(0, 1) + unitName.substring(1).toLowerCase(); String repeaterClassName = RepeaterUnit.class.getPackage().getName() + ".Repeater" + capitalizedUnitName; - return Class.forName(repeaterClassName).asSubclass(RepeaterUnit.class).newInstance(); + return Class.forName(repeaterClassName) + .asSubclass(RepeaterUnit.class) + .getConstructor().newInstance(); } } return null; diff --git a/src/main/java/org/xbib/time/schedule/CronExpression.java b/src/main/java/org/xbib/time/schedule/CronExpression.java new file mode 100644 index 0000000..4ae3d89 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/CronExpression.java @@ -0,0 +1,130 @@ +package org.xbib.time.schedule; + +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class CronExpression { + + public abstract boolean matches(ZonedDateTime t); + + public abstract ZonedDateTime nextExecution(ZonedDateTime from, ZonedDateTime to); + + private static final String YEARLY = "0 0 1 1 *", + MONTHLY = "0 0 1 * *", + WEEKLY = "0 0 * * 7", + DAILY = "0 0 * * *", + HOURLY = "0 * * * *"; + + private static final Map ALIASES = Map.ofEntries( + Map.entry("yearly", YEARLY), + Map.entry("annually", YEARLY), + Map.entry("monthly", MONTHLY), + Map.entry("weekly", WEEKLY), + Map.entry("daily", DAILY), + Map.entry("midnight", DAILY), + Map.entry("hourly", HOURLY)); + + private static final Pattern ALIAS_PATTERN = Pattern.compile("[a-z]+"); + + private static final boolean DEFAULT_ONE_BASED_DAY_OF_WEEK = false; + + private static final boolean DEFAULT_SECONDS = false; + + private static final boolean DEFAULT_ALLOW_BOTH_DAYS = true; + + public static CronExpression yearly() { + return parse(YEARLY); + } + + public static CronExpression monthly() { + return parse(MONTHLY); + } + + public static CronExpression weekly() { + return parse(WEEKLY); + } + + public static CronExpression daily() { + return parse(DAILY); + } + + public static CronExpression hourly() { + return parse(HOURLY); + } + + public static boolean isValid(String s) { + return isValid(s, DEFAULT_ONE_BASED_DAY_OF_WEEK, DEFAULT_SECONDS, DEFAULT_ALLOW_BOTH_DAYS); + } + + public static CronExpression parse(String s) { + return parse(s, DEFAULT_ONE_BASED_DAY_OF_WEEK, DEFAULT_SECONDS, DEFAULT_ALLOW_BOTH_DAYS); + } + + private static boolean isValid(String s, boolean oneBasedDayOfWeek, boolean seconds, boolean allowBothDays) { + boolean valid; + try { + parse(s, oneBasedDayOfWeek, seconds, allowBothDays); + valid = true; + } catch (Exception e) { + valid = false; + } + return valid; + } + + private static CronExpression parse(String s, boolean oneBasedDayOfWeek, boolean seconds, boolean allowBothDays) { + Objects.requireNonNull(s); + if (s.charAt(0) == '@') { + Matcher aliasMatcher = ALIAS_PATTERN.matcher(s); + if (aliasMatcher.find(1)) { + String alias = aliasMatcher.group(); + if (ALIASES.containsKey(alias)) { + return new DefaultCronExpression(ALIASES.get(alias), + DEFAULT_ONE_BASED_DAY_OF_WEEK, DEFAULT_SECONDS, DEFAULT_ALLOW_BOTH_DAYS); + } else if ("reboot".equals(alias)) { + return new RebootCronExpression(); + } + } + } + return new DefaultCronExpression(s, seconds, oneBasedDayOfWeek, allowBothDays); + } + + public static Parser parser() { + return new Parser(); + } + + public static class Parser { + private boolean oneBasedDayOfWeek, seconds, allowBothDays; + + private Parser() { + oneBasedDayOfWeek = DEFAULT_ONE_BASED_DAY_OF_WEEK; + seconds = DEFAULT_SECONDS; + allowBothDays = DEFAULT_ALLOW_BOTH_DAYS; + } + + public boolean isValid(String s) { + return CronExpression.isValid(s, oneBasedDayOfWeek, seconds, allowBothDays); + } + + public CronExpression parse(String s) { + return CronExpression.parse(s, oneBasedDayOfWeek, seconds, allowBothDays); + } + + public Parser withOneBasedDayOfWeek(boolean oneBasedDayOfWeek) { + this.oneBasedDayOfWeek = oneBasedDayOfWeek; + return this; + } + + public Parser withSecondsField(boolean secondsField) { + this.seconds = secondsField; + return this; + } + + public Parser allowBothDayFields(boolean allowBothDayFields) { + this.allowBothDays = allowBothDayFields; + return this; + } + } +} diff --git a/src/main/java/org/xbib/time/schedule/CronSchedule.java b/src/main/java/org/xbib/time/schedule/CronSchedule.java new file mode 100644 index 0000000..7c7d2bf --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/CronSchedule.java @@ -0,0 +1,82 @@ +package org.xbib.time.schedule; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class CronSchedule implements Closeable { + + private final ScheduledExecutorService executor; + + private final List> entries; + + private final int periodInMilliseconds; + + private ScheduledFuture future; + + public CronSchedule(ScheduledExecutorService scheduledExecutorServices) { + this(scheduledExecutorServices, 60000); + } + + public CronSchedule(ScheduledExecutorService scheduledExecutorServices, + int periodInMilliseconds) { + this.executor = scheduledExecutorServices; + this.entries = new ArrayList<>(); + this.periodInMilliseconds = periodInMilliseconds; + } + + public void add(String name, CronExpression expression, Callable callable) { + entries.add(new Entry(name, expression, callable)); + } + + public void remove(String name) { + entries.removeIf(entry -> name.equals(entry.getName())); + } + + public void start() { + long initialDelay = periodInMilliseconds - (Clock.systemDefaultZone().millis() % periodInMilliseconds); + this.future = executor.scheduleAtFixedRate(CronSchedule.this::run, + initialDelay, periodInMilliseconds, TimeUnit.MILLISECONDS); + } + + public void run() { + run(ZonedDateTime.now()); + } + + public void run(ZonedDateTime time) { + for (Entry entry : entries) { + if (entry.getCronExpression().matches(time)) { + entry.setLastCalled(time); + executor.submit(entry.getCallable()); + } + } + } + + @Override + public void close() throws IOException { + if (future != null) { + future.cancel(true); + future = null; + } + if (executor != null) { + executor.shutdownNow(); + try { + executor.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IOException(e); + } + } + } + + @Override + public String toString() { + return entries.toString(); + } +} diff --git a/src/main/java/org/xbib/time/schedule/DayOfMonthField.java b/src/main/java/org/xbib/time/schedule/DayOfMonthField.java new file mode 100644 index 0000000..a0f1c25 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/DayOfMonthField.java @@ -0,0 +1,122 @@ +package org.xbib.time.schedule; + +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAdjusters; + +public class DayOfMonthField extends DefaultField { + + private final boolean lastDay; + + private final boolean nearestWeekday; + + private final boolean unspecified; + + private DayOfMonthField(Builder b) { + super(b); + this.lastDay = b.lastDay; + this.nearestWeekday = b.nearestWeekday; + this.unspecified = b.unspecified; + } + + boolean isUnspecified() { + return unspecified; + } + + public boolean matches(ZonedDateTime time) { + if (unspecified) { + return true; + } + final int dayOfMonth = time.getDayOfMonth(); + if (lastDay) { + return dayOfMonth == time.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); + } else if (nearestWeekday) { + DayOfWeek dayOfWeek = time.getDayOfWeek(); + if ((dayOfWeek == DayOfWeek.MONDAY && contains(time.minusDays(1).getDayOfMonth())) + || (dayOfWeek == DayOfWeek.FRIDAY && contains(time.plusDays(1).getDayOfMonth()))) { + return true; + } + } + return contains(dayOfMonth); + } + + public static DayOfMonthField parse(Tokens s) { + return new Builder().parse(s).build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + DayOfMonthField that = (DayOfMonthField) o; + return lastDay == that.lastDay && nearestWeekday == that.nearestWeekday; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (lastDay ? 1 : 0); + result = 31 * result + (nearestWeekday ? 1 : 0); + return result; + } + + @Override + public String toString() { + return isFullRange() ? "*" : getNumbers().toString(); + } + + public static class Builder extends DefaultField.Builder { + + private boolean lastDay; + + private boolean nearestWeekday; + + private boolean unspecified; + + Builder() { + super(1, 31); + } + + @Override + public DayOfMonthField build() { + return new DayOfMonthField(this); + } + + @Override + protected Builder parse(Tokens tokens) { + super.parse(tokens); + return this; + } + + @Override + protected boolean parseValue(Tokens tokens, Token token, int first, int last) { + if (token == Token.MATCH_ONE) { + unspecified = true; + return false; + } else if (token == Token.LAST) { + lastDay = true; + return false; + } else { + return super.parseValue(tokens, token, first, last); + } + } + + @Override + protected boolean parseNumber(Tokens tokens, Token token, int first, int last) { + if (token == Token.WEEKDAY) { + add(first); + nearestWeekday = true; + return false; + } else { + return super.parseNumber(tokens, token, first, last); + } + } + } +} diff --git a/src/main/java/org/xbib/time/schedule/DayOfWeekField.java b/src/main/java/org/xbib/time/schedule/DayOfWeekField.java new file mode 100644 index 0000000..4451868 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/DayOfWeekField.java @@ -0,0 +1,173 @@ +package org.xbib.time.schedule; + +import org.xbib.time.util.LinkedHashSetMultiMap; +import org.xbib.time.util.MultiMap; +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.LinkedHashSet; +import java.util.Set; + +public class DayOfWeekField extends DefaultField { + + private final MultiMap nth; + + private final Set last; + + private final boolean hasNth; + + private final boolean hasLast; + + private final boolean unspecified; + + private DayOfWeekField(Builder b) { + super(b); + this.nth = b.nth; + hasNth = !nth.isEmpty(); + this.last = b.last; + hasLast = !last.isEmpty(); + unspecified = b.unspecified; + } + + public boolean isUnspecified() { + return unspecified; + } + + public boolean matches(ZonedDateTime time) { + if (unspecified) { + return true; + } + final DayOfWeek dayOfWeek = time.getDayOfWeek(); + //int number = dayOfWeek.getValue() % 7; + int number = dayOfWeek.getValue(); + if (hasLast) { + return last.contains(number) && time.getMonth() != time.plusWeeks(1).getMonth(); + } else if (hasNth) { + int dayOfYear = time.getDayOfYear(); + if (nth.containsKey(number)) { + for (int possibleMatch : nth.get(number)) { + if (dayOfYear == time.with(TemporalAdjusters.dayOfWeekInMonth(possibleMatch, dayOfWeek)).getDayOfYear()) { + return true; + } + } + } + } + return contains(number); + } + + private int number(int dayOfWeek) { + return dayOfWeek % 7; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + DayOfWeekField that = (DayOfWeekField) o; + if (hasLast != that.hasLast) { + return false; + } + if (hasNth != that.hasNth) { + return false; + } + if (!last.equals(that.last)) { + return false; + } + return nth.equals(that.nth); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + nth.hashCode(); + result = 31 * result + last.hashCode(); + result = 31 * result + (hasNth ? 1 : 0); + result = 31 * result + (hasLast ? 1 : 0); + return result; + } + + public static DayOfWeekField parse(Tokens s, boolean oneBased) { + return new Builder(oneBased).parse(s).build(); + } + + public static class Builder extends DefaultField.Builder { + static final Keywords KEYWORDS = new Keywords(); + + static { + KEYWORDS.put("MON", 1); + KEYWORDS.put("TUE", 2); + KEYWORDS.put("WED", 3); + KEYWORDS.put("THU", 4); + KEYWORDS.put("FRI", 5); + KEYWORDS.put("SAT", 6); + KEYWORDS.put("SUN", 7); + } + + private boolean oneBased; + + private boolean unspecified; + + private final Set last; + + private final MultiMap nth; + + Builder(boolean oneBased) { + super(1, 7); + this.oneBased = oneBased; + last = new LinkedHashSet<>(); + nth = new LinkedHashSetMultiMap<>(); + } + + @Override + protected Builder parse(Tokens tokens) { + tokens.keywords(KEYWORDS); + if (oneBased) { + tokens.offset(1); + } + super.parse(tokens); + tokens.reset(); + return this; + } + + @Override + protected boolean parseValue(Tokens tokens, Token token, int first, int last) { + if (token == Token.MATCH_ONE) { + unspecified = true; + return false; + } else { + return super.parseValue(tokens, token, first, last); + } + } + + @Override + protected boolean parseNumber(Tokens tokens, Token token, int first, int last) { + if (token == Token.LAST) { + this.last.add(first); + } else if (token == Token.NTH) { + int number = nextNumber(tokens); + if (oneBased) { + number += 1; + } + if (number == 0) { + number = 7; + } + nth.put(first, number); + } else { + return super.parseNumber(tokens, token, first, last); + } + return false; + } + + @Override + public DayOfWeekField build() { + return new DayOfWeekField(this); + } + } +} diff --git a/src/main/java/org/xbib/time/schedule/DefaultCronExpression.java b/src/main/java/org/xbib/time/schedule/DefaultCronExpression.java new file mode 100644 index 0000000..11bc125 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/DefaultCronExpression.java @@ -0,0 +1,228 @@ +package org.xbib.time.schedule; + +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.Objects; +import java.util.SortedSet; +import java.util.logging.Logger; + +public class DefaultCronExpression extends CronExpression { + + private final String string; + + private final TimeField second; + + private final TimeField minute; + + private final TimeField hour; + + private final TimeField month; + + private final TimeField year; + + private final DayOfWeekField dayOfWeek; + + private final DayOfMonthField dayOfMonth; + + DefaultCronExpression(String string, boolean seconds, boolean oneBasedDayOfWeek, boolean allowBothDayFields) { + this.string = string; + if (string.isEmpty()) { + throw new IllegalArgumentException("empty spec not allowed"); + } + String s = string.toUpperCase(); + Tokens tokens = new Tokens(s); + if (seconds) { + second = DefaultField.parse(tokens, 0, 59); + } else { + second = MatchAllField.instance; + } + minute = DefaultField.parse(tokens, 0, 59); + hour = DefaultField.parse(tokens, 0, 23); + dayOfMonth = DayOfMonthField.parse(tokens); + month = MonthField.parse(tokens); + dayOfWeek = DayOfWeekField.parse(tokens, oneBasedDayOfWeek); + if (tokens.hasNext()) { + year = DefaultField.parse(tokens, 0, 0); + } else { + year = MatchAllField.instance; + } + if (!allowBothDayFields && !dayOfMonth.isUnspecified() && !dayOfWeek.isUnspecified()) { + throw new IllegalArgumentException("Day of month and day of week may not both be specified"); + } + } + + @Override + public boolean matches(ZonedDateTime t) { + return second.contains(t.getSecond()) && + minute.contains(t.getMinute()) && + hour.contains(t.getHour()) && + month.contains(t.getMonthValue()) && + year.contains(t.getYear()) && + dayOfWeek.matches(t) && + dayOfMonth.matches(t); + } + + @Override + public ZonedDateTime nextExecution(ZonedDateTime from, + ZonedDateTime to) { + ZonedDateTime next = second instanceof MatchAllField ? + from.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES) : from.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS); + while (true) { + if (next.isBefore(from) || next.isAfter(to)) { + throw new IllegalStateException("out of range: " + from + " < " + next + " < " + to + " -> " + this); + } + SortedSet set; + if (!year.contains(next.getYear())) { + if (!year.isFullRange()) { + set = year.getNumbers().tailSet(next.getYear()); + if (set.isEmpty()) { + next = next.plusYears(1); + continue; + } else { + next = next.plusYears(set.first() - next.getYear()); + } + } + } + if (!month.contains(next.getMonthValue())) { + if (!month.isFullRange()) { + set = month.getNumbers().tailSet(next.getMonthValue()); + if (set.isEmpty()) { + next = next.plusMonths(1); + continue; + } else { + next = next.plusMonths(set.first() - next.getMonthValue()); + } + } + } + if (!dayOfMonth.isUnspecified()) { + if (!dayOfMonth.contains(next.getDayOfMonth())) { + if (!dayOfMonth.isFullRange()) { + set = dayOfMonth.getNumbers().tailSet(next.getDayOfMonth()); + if (set.isEmpty()) { + next = next.plusDays(1).truncatedTo(ChronoUnit.DAYS); + continue; + } else { + next = next.plusDays(set.first() - next.getDayOfMonth()) + .truncatedTo(ChronoUnit.DAYS); + } + } + } + } + if (!dayOfWeek.isUnspecified()) { + if (!dayOfWeek.contains(next.getDayOfWeek().getValue())) { + if (!dayOfWeek.isFullRange()) { + set = dayOfWeek.getNumbers().tailSet(next.getDayOfWeek().getValue()); + if (set.isEmpty()) { + next = next.plusDays(1).truncatedTo(ChronoUnit.DAYS); + continue; + } else { + DayOfWeek dayOfWeek = DayOfWeek.of(set.first()); + next = next.with(TemporalAdjusters.next(dayOfWeek)); + + } + } + } + } + if (!hour.contains(next.getHour())) { + if (!hour.isFullRange()) { + set = hour.getNumbers().tailSet(next.getHour()); + if (set.isEmpty()) { + next = next.plusHours(1).truncatedTo(ChronoUnit.HOURS); + continue; + } else { + next = next.plusHours(set.first() - next.getHour()) + .truncatedTo(ChronoUnit.HOURS); + } + } + } + if (!minute.contains(next.getMinute())) { + if (!minute.isFullRange()) { + set = minute.getNumbers().tailSet(next.getMinute()); + if (set.isEmpty()) { + next = next.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES); + continue; + } else { + next = next.plusMinutes(set.first() - next.getMinute()) + .truncatedTo(ChronoUnit.MINUTES); + } + } + } + if (!(second instanceof MatchAllField)) { + if (!second.contains(next.getSecond())) { + if (!second.isFullRange()) { + set = second.getNumbers().tailSet(next.getSecond()); + if (set.isEmpty()) { + next = next.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS); + continue; + } else { + next = next.plusSeconds(set.first() - next.getSecond()) + .truncatedTo(ChronoUnit.SECONDS); + } + } + } + } + break; + } + return next; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultCronExpression that = (DefaultCronExpression) o; + if (!Objects.equals(dayOfMonth, that.dayOfMonth)) { + return false; + } + if (!Objects.equals(dayOfWeek, that.dayOfWeek)) { + return false; + } + if (!Objects.equals(hour, that.hour)) { + return false; + } + if (!Objects.equals(minute, that.minute)) { + return false; + } + if (!Objects.equals(month, that.month)) { + return false; + } + if (!Objects.equals(second, that.second)) { + return false; + } + if (!string.equals(that.string)) { + return false; + } + return Objects.equals(year, that.year); + } + + @Override + public int hashCode() { + int result = string.hashCode(); + result = 31 * result + (second != null ? second.hashCode() : 0); + result = 31 * result + (minute != null ? minute.hashCode() : 0); + result = 31 * result + (hour != null ? hour.hashCode() : 0); + result = 31 * result + (month != null ? month.hashCode() : 0); + result = 31 * result + (year != null ? year.hashCode() : 0); + result = 31 * result + (dayOfWeek != null ? dayOfWeek.hashCode() : 0); + result = 31 * result + (dayOfMonth != null ? dayOfMonth.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "CronExpression[" + string + " -> seconds=" + second + + ",mins=" + minute + + ",hrs=" + hour + + ",dayOfMonths=" + dayOfMonth + + ",dayOfWeek=" + dayOfWeek + + ",months=" + month + + ",yrs=" + year + + "]"; + } +} diff --git a/src/main/java/org/xbib/time/schedule/DefaultField.java b/src/main/java/org/xbib/time/schedule/DefaultField.java new file mode 100644 index 0000000..c7a1715 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/DefaultField.java @@ -0,0 +1,177 @@ +package org.xbib.time.schedule; + +import java.util.NavigableSet; +import java.util.Objects; +import java.util.TreeSet; + +public class DefaultField implements TimeField { + + private final boolean fullRange; + + private final NavigableSet numbers; + + DefaultField(Builder b) { + fullRange = b.fullRange; + numbers = fullRange ? null : b.numbers; + } + + public static DefaultField parse(Tokens s, int min, int max) { + return new Builder(min, max).parse(s).build(); + } + + @Override + public boolean contains(int number) { + return fullRange || numbers.contains(number); + } + + @Override + public NavigableSet getNumbers() { + return numbers; + } + + @Override + public boolean isFullRange() { + return fullRange; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultField that = (DefaultField) o; + if (fullRange != that.fullRange) { + return false; + } + return Objects.equals(numbers, that.numbers); + } + + @Override + public int hashCode() { + int result = (fullRange ? 1 : 0); + result = 31 * result + (numbers != null ? numbers.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return isFullRange() ? "*" : getNumbers().toString(); + } + + public static class Builder { + + private final NavigableSet numbers; + + private final int min; + + private final int max; + + private boolean fullRange; + + Builder(int min, int max) { + this.min = min; + this.max = max; + numbers = new TreeSet<>(); + } + + protected Builder parse(Tokens tokens) { + Token token; + while (!endOfField(token = tokens.next())) { + if (parseValue(tokens, token, min, max)) { + break; + } + } + return this; + } + + protected boolean parseValue(Tokens tokens, Token token, int first, int last) { + if (token == Token.NUMBER) { + return parseNumber(tokens, tokens.next(), tokens.number(), last); + } else if (token == Token.MATCH_ALL) { + token = tokens.next(); + if (token == Token.SKIP) { + rangeSkip(first, last, nextNumber(tokens)); + } else if (token == Token.VALUE_SEPARATOR) { + range(first, last); + } else if (endOfField(token)) { + range(first, last); + return true; + } + } + return false; + } + + /** + * Returns true if the end of this field has been reached. + * @param tokens tokens + * @param token token + * @param first first + * @param last last + * @return true if end reached + */ + protected boolean parseNumber(Tokens tokens, Token token, int first, int last) { + Token t = token; + int l = last; + if (t == Token.SKIP) { + rangeSkip(first, l, nextNumber(tokens)); + } else if (t == Token.RANGE) { + l = nextNumber(tokens); + t = tokens.next(); + if (t == Token.SKIP) { + rangeSkip(first, l, nextNumber(tokens)); + } else if (t == Token.VALUE_SEPARATOR) { + range(first, l); + } else if (endOfField(t)) { + range(first, l); + return true; + } + } else if (t == Token.VALUE_SEPARATOR) { + add(first); + } else if (endOfField(t)) { + add(first); + return true; + } + return false; + } + + int nextNumber(Tokens tokens) { + if (tokens.next() == Token.NUMBER) { + return tokens.number(); + } + throw new IllegalStateException("Expected number"); + } + + private boolean endOfField(Token token) { + return token == Token.FIELD_SEPARATOR || token == Token.END_OF_INPUT; + } + + void rangeSkip(int first, int last, int skip) { + for (int i = first; i <= last; i++) { + if ((i - min) % skip == 0) { + add(i); + } + } + } + + protected void range(int first, int last) { + if (first == min && last == max) { + fullRange = true; + } else { + for (int i = first; i <= last; i++) { + add(i); + } + } + } + + protected void add(int value) { + numbers.add(value); + } + + public DefaultField build() { + return new DefaultField(this); + } + } +} diff --git a/src/main/java/org/xbib/time/schedule/Entry.java b/src/main/java/org/xbib/time/schedule/Entry.java new file mode 100644 index 0000000..776498d --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/Entry.java @@ -0,0 +1,57 @@ +package org.xbib.time.schedule; + +import java.time.ZonedDateTime; +import java.util.concurrent.Callable; + +class Entry { + + private String name; + + private CronExpression cronExpression; + + private Callable callable; + + private ZonedDateTime lastCalled; + + private ZonedDateTime nextCall; + + Entry(String name, CronExpression cronExpression, Callable callable) { + this.name = name; + this.cronExpression = cronExpression; + this.callable = callable; + } + + public String getName() { + return name; + } + + public CronExpression getCronExpression() { + return cronExpression; + } + + public Callable getCallable() { + return callable; + } + + public void setLastCalled(ZonedDateTime lastCalled) { + this.lastCalled = lastCalled; + // heuristic, limit to 1 year ahead + this.nextCall = cronExpression.nextExecution(lastCalled, lastCalled.plusYears(1)); + } + + public ZonedDateTime getLastCalled() { + return lastCalled; + } + + public ZonedDateTime getNextCall() { + return nextCall; + } + + @Override + public String toString() { + return "Entry[name=" + name + ", expression=" + cronExpression + + ",callable= " + callable + + ",lastcalled=" + lastCalled + + ",nextcall=" + nextCall + "]"; + } +} diff --git a/src/main/java/org/xbib/time/schedule/Keywords.java b/src/main/java/org/xbib/time/schedule/Keywords.java new file mode 100644 index 0000000..ed6fa29 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/Keywords.java @@ -0,0 +1,42 @@ +package org.xbib.time.schedule; + +import java.util.Arrays; + +final class Keywords { + private final int[][][] keywords = new int[26][26][26]; + + public Keywords() { + for (int[][] second : keywords) { + for (int[] third : second) { + Arrays.fill(third, -1); + } + } + } + + public void put(String keyword, int value) { + keywords[letterAt(keyword, 0)][letterAt(keyword, 1)][letterAt(keyword, 2)] = value; + } + + public int get(String s, int start, int end) { + if (end - start != 3) { + throw new IllegalArgumentException(); + } + int number = keywords[arrayIndex(s, start)][arrayIndex(s, start + 1)][arrayIndex(s, start + 2)]; + if (number >= 0) { + return number; + } + throw new IllegalArgumentException(); + } + + private int arrayIndex(String s, int charIndex) { + int index = letterAt(s, charIndex); + if (index < 0 || index >= keywords.length) { + throw new IllegalArgumentException(); + } + return index; + } + + private static int letterAt(String s, int charIndex) { + return s.charAt(charIndex) - 'A'; + } +} \ No newline at end of file diff --git a/src/main/java/org/xbib/time/schedule/MatchAllField.java b/src/main/java/org/xbib/time/schedule/MatchAllField.java new file mode 100644 index 0000000..09d3831 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/MatchAllField.java @@ -0,0 +1,23 @@ +package org.xbib.time.schedule; + +import java.util.NavigableSet; + +public enum MatchAllField implements TimeField { + + instance; + + @Override + public boolean contains(int number) { + return true; + } + + @Override + public NavigableSet getNumbers() { + return null; + } + + @Override + public boolean isFullRange() { + return true; + } +} diff --git a/src/main/java/org/xbib/time/schedule/MonthField.java b/src/main/java/org/xbib/time/schedule/MonthField.java new file mode 100644 index 0000000..8dc91a8 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/MonthField.java @@ -0,0 +1,48 @@ +package org.xbib.time.schedule; + +public class MonthField extends DefaultField { + + private MonthField(Builder b) { + super(b); + } + + public static MonthField parse(Tokens s) { + return new Builder().parse(s).build(); + } + + public static class Builder extends DefaultField.Builder { + protected static final Keywords KEYWORDS = new Keywords(); + + static { + KEYWORDS.put("JAN", 1); + KEYWORDS.put("FEB", 2); + KEYWORDS.put("MAR", 3); + KEYWORDS.put("APR", 4); + KEYWORDS.put("MAY", 5); + KEYWORDS.put("JUN", 6); + KEYWORDS.put("JUL", 7); + KEYWORDS.put("AUG", 8); + KEYWORDS.put("SEP", 9); + KEYWORDS.put("OCT", 10); + KEYWORDS.put("NOV", 11); + KEYWORDS.put("DEC", 12); + } + + Builder() { + super(1, 12); + } + + @Override + protected Builder parse(Tokens tokens) { + tokens.keywords(KEYWORDS); + super.parse(tokens); + tokens.reset(); + return this; + } + + @Override + public MonthField build() { + return new MonthField(this); + } + } +} diff --git a/src/main/java/org/xbib/time/schedule/RebootCronExpression.java b/src/main/java/org/xbib/time/schedule/RebootCronExpression.java new file mode 100644 index 0000000..311d854 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/RebootCronExpression.java @@ -0,0 +1,26 @@ +package org.xbib.time.schedule; + +import java.time.ZonedDateTime; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Matches once only. + */ +final class RebootCronExpression extends CronExpression { + + private final AtomicBoolean matchOnce; + + RebootCronExpression() { + matchOnce = new AtomicBoolean(true); + } + + @Override + public boolean matches(ZonedDateTime t) { + return matchOnce.getAndSet(false); + } + + @Override + public ZonedDateTime nextExecution(ZonedDateTime from, ZonedDateTime to) { + return null; + } +} diff --git a/src/main/java/org/xbib/time/schedule/TimeField.java b/src/main/java/org/xbib/time/schedule/TimeField.java new file mode 100644 index 0000000..2eae374 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/TimeField.java @@ -0,0 +1,12 @@ +package org.xbib.time.schedule; + +import java.util.NavigableSet; + +public interface TimeField { + + boolean contains(int number); + + NavigableSet getNumbers(); + + boolean isFullRange(); +} diff --git a/src/main/java/org/xbib/time/schedule/Token.java b/src/main/java/org/xbib/time/schedule/Token.java new file mode 100644 index 0000000..c5eb584 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/Token.java @@ -0,0 +1,15 @@ +package org.xbib.time.schedule; + +enum Token { + END_OF_INPUT, + FIELD_SEPARATOR, + LAST, + MATCH_ALL, + MATCH_ONE, + NTH, + NUMBER, + RANGE, + SKIP, + VALUE_SEPARATOR, + WEEKDAY +} diff --git a/src/main/java/org/xbib/time/schedule/Tokens.java b/src/main/java/org/xbib/time/schedule/Tokens.java new file mode 100644 index 0000000..ad950f0 --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/Tokens.java @@ -0,0 +1,189 @@ +package org.xbib.time.schedule; + +final class Tokens { + + private int number; + + private int offset; + + private Keywords keywords; + + private final String source; + private final int length; + private int position; + + public Tokens(String s) { + source = s; + length = s.length(); + position = 0; + } + + public int number() { + return number; + } + + public void offset(int offset) { + this.offset = offset; + } + + public void keywords(Keywords k) { + keywords = k; + } + + public void reset() { + offset = 0; + keywords = null; + } + + public boolean hasNext() { + return hasNextChar(); + } + + public Token next() { + if (position >= length) { + return Token.END_OF_INPUT; + } + int start = position; + char c = currentChar(); + switch (c) { + case ' ': + case '\t': + do { + if (!hasNextChar()) { + position++; + break; + } + c = nextChar(); + } while (isWhitespace(c)); + return Token.FIELD_SEPARATOR; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + do { + if (!hasNextChar()) { + position++; + break; + } + c = nextChar(); + } while (isDigit(c)); + number = Integer.parseInt(substringFrom(start)) - offset; + return Token.NUMBER; + case ',': + position++; + return Token.VALUE_SEPARATOR; + case '*': + position++; + return Token.MATCH_ALL; + case '-': + position++; + return Token.RANGE; + case '/': + position++; + return Token.SKIP; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + do { + if (!hasNextChar()) { + position++; + break; + } + c = nextChar(); + } while (isLetter(c)); + if (position - start == 1) { + c = source.charAt(start); + if (c == 'L') { + return Token.LAST; + } else if (c == 'W') { + return Token.WEEKDAY; + } + throw new IllegalArgumentException(badCharacter(c, start)); + } else { + if (keywords != null) { + try { + int mapped = keywords.get(source, start, position); + if (mapped != -1) { + number = mapped; + return Token.NUMBER; + } + } catch (IllegalArgumentException ignore) { + } + } + throw new IllegalArgumentException(badKeyword(start)); + } + case '?': + position++; + return Token.MATCH_ONE; + case '#': + position++; + return Token.NTH; + } + throw new IllegalArgumentException(badCharacter(c, position)); + } + + private String badCharacter(char c, int index) { + return "Bad character '" + c + "' at position " + index + " in string: " + source; + } + + private String badKeyword(int start) { + return "Bad keyword '" + substringFrom(start) + "' at position " + start + " in string: " + source; + } + + private String substringFrom(int start) { + return source.substring(start, position); + } + + private boolean hasNextChar() { + return position < length - 1; + } + + private char nextChar() { + return source.charAt(++position); + } + + private char currentChar() { + return source.charAt(position); + } + + private static boolean isLetter(char c) { + return 'A' <= c && c <= 'Z'; + } + + private static boolean isDigit(char c) { + return '0' <= c && c <= '9'; + } + + private static boolean isWhitespace(char c) { + return c == ' ' || c == '\t'; + } +} diff --git a/src/main/java/org/xbib/time/schedule/package-info.java b/src/main/java/org/xbib/time/schedule/package-info.java new file mode 100644 index 0000000..15b052e --- /dev/null +++ b/src/main/java/org/xbib/time/schedule/package-info.java @@ -0,0 +1,4 @@ +/** + * Schedule jobs (like cron). + */ +package org.xbib.time.schedule; diff --git a/src/main/java/org/xbib/time/util/AbstractMultiMap.java b/src/main/java/org/xbib/time/util/AbstractMultiMap.java new file mode 100644 index 0000000..41a2b30 --- /dev/null +++ b/src/main/java/org/xbib/time/util/AbstractMultiMap.java @@ -0,0 +1,126 @@ +package org.xbib.time.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Abstract multi map. + * + * @param the key type parameter + * @param the value type parameter + */ +abstract class AbstractMultiMap implements MultiMap { + + private final Map> map; + + AbstractMultiMap() { + this(null); + } + + private AbstractMultiMap(MultiMap map) { + this.map = newMap(); + if (map != null) { + putAll(map); + } + } + + @Override + public int size() { + return map.size(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(K key) { + return map.containsKey(key); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public boolean put(K key, V value) { + Collection set = map.get(key); + if (set == null) { + set = newValues(); + set.add(value); + map.put(key, set); + return true; + } else { + set.add(value); + return false; + } + } + + @Override + public void putAll(K key, Iterable values) { + if (values == null) { + return; + } + Collection set = map.computeIfAbsent(key, k -> newValues()); + for (V v : values) { + set.add(v); + } + } + + @Override + public Collection get(K key) { + return map.get(key); + } + + @Override + public Collection remove(K key) { + return map.remove(key); + } + + @Override + public boolean remove(K key, V value) { + Collection set = map.get(key); + return set != null && set.remove(value); + } + + @Override + public void putAll(MultiMap map) { + if (map != null) { + for (K key : map.keySet()) { + putAll(key, map.get(key)); + } + } + } + + @Override + public Map> asMap() { + return map; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AbstractMultiMap && map.equals(((AbstractMultiMap) obj).map); + } + + @Override + public int hashCode() { + return map.hashCode(); + } + + @Override + public String toString() { + return map.toString(); + } + + abstract Collection newValues(); + + abstract Map> newMap(); +} diff --git a/src/main/java/org/xbib/time/util/LinkedHashSetMultiMap.java b/src/main/java/org/xbib/time/util/LinkedHashSetMultiMap.java new file mode 100644 index 0000000..80fad1a --- /dev/null +++ b/src/main/java/org/xbib/time/util/LinkedHashSetMultiMap.java @@ -0,0 +1,29 @@ +package org.xbib.time.util; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +/** + * Linked multi map. + * + * @param the key type parameter + * @param the value type parameter + */ +public class LinkedHashSetMultiMap extends AbstractMultiMap { + + public LinkedHashSetMultiMap() { + super(); + } + + @Override + Collection newValues() { + return new LinkedHashSet<>(); + } + + @Override + Map> newMap() { + return new LinkedHashMap<>(); + } +} diff --git a/src/main/java/org/xbib/time/util/MultiMap.java b/src/main/java/org/xbib/time/util/MultiMap.java new file mode 100644 index 0000000..9f1df0e --- /dev/null +++ b/src/main/java/org/xbib/time/util/MultiMap.java @@ -0,0 +1,38 @@ +package org.xbib.time.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * MultiMap interface. + * + * @param the key type parameter + * @param the value type parameter + */ +public interface MultiMap { + + void clear(); + + int size(); + + boolean isEmpty(); + + boolean containsKey(K key); + + Collection get(K key); + + Set keySet(); + + boolean put(K key, V value); + + void putAll(K key, Iterable values); + + void putAll(MultiMap map); + + Collection remove(K key); + + boolean remove(K key, V value); + + Map> asMap(); +} diff --git a/src/main/java/org/xbib/time/util/TreeMultiMap.java b/src/main/java/org/xbib/time/util/TreeMultiMap.java new file mode 100644 index 0000000..89ef157 --- /dev/null +++ b/src/main/java/org/xbib/time/util/TreeMultiMap.java @@ -0,0 +1,31 @@ +package org.xbib.time.util; + +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.TreeMap; + +/** + * A {@link TreeMap} based multi map. The keys ore ordered by a comparator. + * @param te key type + * @param the value type + */ +public class TreeMultiMap extends AbstractMultiMap { + + private final Comparator comparator; + + public TreeMultiMap(Comparator comparator) { + this.comparator = comparator; + } + + @Override + Map> newMap() { + return new TreeMap<>(comparator); + } + + @Override + Collection newValues() { + return new LinkedHashSet<>(); + } +} diff --git a/src/main/java/org/xbib/time/util/package-info.java b/src/main/java/org/xbib/time/util/package-info.java new file mode 100644 index 0000000..5163410 --- /dev/null +++ b/src/main/java/org/xbib/time/util/package-info.java @@ -0,0 +1,4 @@ +/** + * + */ +package org.xbib.time.util; diff --git a/src/test/java/org/xbib/time/FormatterTest.java b/src/test/java/org/xbib/time/DateTimeFormatterTest.java similarity index 95% rename from src/test/java/org/xbib/time/FormatterTest.java rename to src/test/java/org/xbib/time/DateTimeFormatterTest.java index 83f15d8..fd67d18 100644 --- a/src/test/java/org/xbib/time/FormatterTest.java +++ b/src/test/java/org/xbib/time/DateTimeFormatterTest.java @@ -10,7 +10,7 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; -public class FormatterTest { +public class DateTimeFormatterTest { @Test public void testLocalDate() { diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeAPIManipulationTest.java b/src/test/java/org/xbib/time/pretty/PrettyTimeAPIManipulationTest.java index b61d578..4164d38 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeAPIManipulationTest.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeAPIManipulationTest.java @@ -10,8 +10,10 @@ import static org.junit.Assert.assertNotNull; public class PrettyTimeAPIManipulationTest { - List list = null; - PrettyTime t = new PrettyTime(); + private List list = null; + + private PrettyTime t = new PrettyTime(); + private TimeUnitQuantity timeUnitQuantity = null; @Test diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_AR_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_AR_Test.java index 7f2675c..8159155 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_AR_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_AR_Test.java @@ -1,250 +1,252 @@ -package org.xbib.time.pretty; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoField; -import java.util.List; -import java.util.Locale; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class PrettyTimeI18n_AR_Test { - - private Locale locale; - - @Before - public void setUp() throws Exception { - locale = new Locale("ar"); - Locale.setDefault(locale); - } - - @Test - public void testCeilingInterval() throws Exception { - LocalDateTime localDateTime = LocalDateTime.of(2009, 6, 17, 0, 0); - PrettyTime p = new PrettyTime(localDateTime); - assertEquals("1 شهر مضت", p.format(LocalDateTime.of(2009, 5, 20, 0, 0))); - } - - @Test - public void testRightNow() throws Exception { - PrettyTime t = new PrettyTime(); - assertEquals("بعد لحظات", t.format(LocalDateTime.now())); - } - - @Test - public void testRightNowVariance() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("بعد لحظات", t.format(600)); - } - - @Test - public void testMinutesFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("12 دقائق من الآن", t.format(1000 * 60 * 12)); - } - - @Test - public void testHoursFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 ساعات من الآن", t.format(1000 * 60 * 60 * 3)); - } - - @Test - public void testDaysFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 ايام من الآن", t.format(1000 * 60 * 60 * 24 * 3)); - } - - @Test - public void testWeeksFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 أسابيع من الآن", t.format(1000 * 60 * 60 * 24 * 7 * 3)); - } - - @Test - public void testMonthsFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 أشهر من الآن", t.format(2629743830L * 3L)); - } - - @Test - public void testYearsFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 سنوات من الآن", t.format(2629743830L * 12L * 3L)); - } - - @Test - public void testDecadesFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 عقود من الآن", t.format(315569259747L * 3L)); - } - - @Test - public void testCenturiesFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 قرون من الآن", t.format(3155692597470L * 3L)); - } - - @Test - public void testMomentsAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(6000), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("منذ لحظات", t.format(0)); - } - - @Test - public void testMinutesAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 12), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("12 دقائق مضت", t.format(0)); - } - - @Test - public void testHoursAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 3), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 ساعات مضت", t.format(0)); - } - - @Test - public void testDaysAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 ايام مضت", t.format(0)); - } - - @Test - public void testWeeksAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 7 * 3), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 أسابيع مضت", t.format(0)); - } - - @Test - public void testMonthsAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 أشهر مضت", t.format(0)); - } - - @Test - public void testCustomFormat() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - TimeUnit unit = new TimeUnit() { - @Override - public long getMaxQuantity() { - return 0; - } - - @Override - public long getMillisPerUnit() { - return 5000; - } - }; - t.clearUnits(); - t.registerUnit(unit, new SimpleTimeFormat() - .setSingularName("tick").setPluralName("ticks") - .setPattern("%n %u").setRoundingTolerance(20) - .setFutureSuffix("... RUN!") - .setFuturePrefix("self destruct in: ").setPastPrefix("self destruct was: ").setPastSuffix( - " ago...")); - - assertEquals("self destruct in: 5 ticks ... RUN!", t.format(25000)); - localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(25000), ZoneId.systemDefault()); - t.setReference(localDateTime); - assertEquals("self destruct was: 5 ticks ago...", t.format(0)); - } - - @Test - public void testYearsAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 12L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 سنوات مضت", t.format(0)); - } - - @Test - public void testDecadesAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(315569259747L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 عقود مضت", t.format(0)); - } - - @Test - public void testCenturiesAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 قرون مضت", t.format(0)); - } - - @Test - public void testWithinTwoHoursRounding() throws Exception { - PrettyTime t = new PrettyTime(); - LocalDateTime localDateTime = LocalDateTime.now().minus(6543990, ChronoField.MILLI_OF_SECOND.getBaseUnit()); - assertEquals("2 ساعات مضت", t.format(localDateTime)); - } - - @Test - public void testPreciseInTheFuture() throws Exception { - PrettyTime t = new PrettyTime(); - LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(10 * 60 + 5 * 60 * 60); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertTrue(timeUnitQuantities.size() >= 2); - assertEquals(5, timeUnitQuantities.get(0).getQuantity()); - assertEquals(10, timeUnitQuantities.get(1).getQuantity()); - } - - @Test - public void testPreciseInThePast() throws Exception { - PrettyTime t = new PrettyTime(); - LocalDateTime localDateTime = LocalDateTime.now().minusSeconds(10 * 60 + 5 * 60 * 60); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertTrue(timeUnitQuantities.size() >= 2); - assertEquals(-5, timeUnitQuantities.get(0).getQuantity()); - assertTrue(timeUnitQuantities.get(1).getQuantity() == -9 || timeUnitQuantities.get(1).getQuantity() == -10); - } - - @Test - public void testFormattingDurationListInThePast() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - List timeUnitQuantities = t.calculatePreciseDuration(LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault())); - assertEquals("3 ايام 15 ساعات 38 دقائق مضت", t.format(timeUnitQuantities)); - } - - @Test - public void testFormattingDurationListInTheFuture() throws Exception { - PrettyTime t = new PrettyTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault())); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38), ZoneId.systemDefault()); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertEquals("3 ايام 15 ساعات 38 دقائق من الآن", t.format(timeUnitQuantities)); - } - - @Test - public void testSetLocale() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(315569259747L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("3 عقود مضت", t.format(0)); - } - - @After - public void tearDown() throws Exception { - Locale.setDefault(locale); - } - -} +package org.xbib.time.pretty; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoField; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PrettyTimeI18n_AR_Test { + + private Locale defaultLocale; + + private Locale locale; + + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("ar"); + Locale.setDefault(locale); + } + + @Test + public void testCeilingInterval() throws Exception { + LocalDateTime localDateTime = LocalDateTime.of(2009, 6, 17, 0, 0); + PrettyTime p = new PrettyTime(localDateTime); + assertEquals("1 شهر مضت", p.format(LocalDateTime.of(2009, 5, 20, 0, 0))); + } + + @Test + public void testRightNow() throws Exception { + PrettyTime t = new PrettyTime(); + assertEquals("بعد لحظات", t.format(LocalDateTime.now())); + } + + @Test + public void testRightNowVariance() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("بعد لحظات", t.format(600)); + } + + @Test + public void testMinutesFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("12 دقائق من الآن", t.format(1000 * 60 * 12)); + } + + @Test + public void testHoursFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 ساعات من الآن", t.format(1000 * 60 * 60 * 3)); + } + + @Test + public void testDaysFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 ايام من الآن", t.format(1000 * 60 * 60 * 24 * 3)); + } + + @Test + public void testWeeksFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 أسابيع من الآن", t.format(1000 * 60 * 60 * 24 * 7 * 3)); + } + + @Test + public void testMonthsFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 أشهر من الآن", t.format(2629743830L * 3L)); + } + + @Test + public void testYearsFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 سنوات من الآن", t.format(2629743830L * 12L * 3L)); + } + + @Test + public void testDecadesFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 عقود من الآن", t.format(315569259747L * 3L)); + } + + @Test + public void testCenturiesFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 قرون من الآن", t.format(3155692597470L * 3L)); + } + + @Test + public void testMomentsAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(6000), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("منذ لحظات", t.format(0)); + } + + @Test + public void testMinutesAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 12), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("12 دقائق مضت", t.format(0)); + } + + @Test + public void testHoursAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 3), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 ساعات مضت", t.format(0)); + } + + @Test + public void testDaysAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 ايام مضت", t.format(0)); + } + + @Test + public void testWeeksAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 7 * 3), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 أسابيع مضت", t.format(0)); + } + + @Test + public void testMonthsAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 أشهر مضت", t.format(0)); + } + + @Test + public void testCustomFormat() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + TimeUnit unit = new TimeUnit() { + @Override + public long getMaxQuantity() { + return 0; + } + + @Override + public long getMillisPerUnit() { + return 5000; + } + }; + t.clearUnits(); + t.registerUnit(unit, new SimpleTimeFormat() + .setSingularName("tick").setPluralName("ticks") + .setPattern("%n %u").setRoundingTolerance(20) + .setFutureSuffix("... RUN!") + .setFuturePrefix("self destruct in: ").setPastPrefix("self destruct was: ").setPastSuffix( + " ago...")); + + assertEquals("self destruct in: 5 ticks ... RUN!", t.format(25000)); + localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(25000), ZoneId.systemDefault()); + t.setReference(localDateTime); + assertEquals("self destruct was: 5 ticks ago...", t.format(0)); + } + + @Test + public void testYearsAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 12L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 سنوات مضت", t.format(0)); + } + + @Test + public void testDecadesAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(315569259747L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 عقود مضت", t.format(0)); + } + + @Test + public void testCenturiesAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 قرون مضت", t.format(0)); + } + + @Test + public void testWithinTwoHoursRounding() throws Exception { + PrettyTime t = new PrettyTime(); + LocalDateTime localDateTime = LocalDateTime.now().minus(6543990, ChronoField.MILLI_OF_SECOND.getBaseUnit()); + assertEquals("2 ساعات مضت", t.format(localDateTime)); + } + + @Test + public void testPreciseInTheFuture() throws Exception { + PrettyTime t = new PrettyTime(); + LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(10 * 60 + 5 * 60 * 60); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertTrue(timeUnitQuantities.size() >= 2); + assertEquals(5, timeUnitQuantities.get(0).getQuantity()); + assertEquals(10, timeUnitQuantities.get(1).getQuantity()); + } + + @Test + public void testPreciseInThePast() throws Exception { + PrettyTime t = new PrettyTime(); + LocalDateTime localDateTime = LocalDateTime.now().minusSeconds(10 * 60 + 5 * 60 * 60); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertTrue(timeUnitQuantities.size() >= 2); + assertEquals(-5, timeUnitQuantities.get(0).getQuantity()); + assertTrue(timeUnitQuantities.get(1).getQuantity() == -9 || timeUnitQuantities.get(1).getQuantity() == -10); + } + + @Test + public void testFormattingDurationListInThePast() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + List timeUnitQuantities = t.calculatePreciseDuration(LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault())); + assertEquals("3 ايام 15 ساعات 38 دقائق مضت", t.format(timeUnitQuantities)); + } + + @Test + public void testFormattingDurationListInTheFuture() throws Exception { + PrettyTime t = new PrettyTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault())); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38), ZoneId.systemDefault()); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertEquals("3 ايام 15 ساعات 38 دقائق من الآن", t.format(timeUnitQuantities)); + } + + @Test + public void testSetLocale() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(315569259747L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("3 عقود مضت", t.format(0)); + } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_BG_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_BG_Test.java index 0ddcc86..dd9cd16 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_BG_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_BG_Test.java @@ -1,224 +1,224 @@ -package org.xbib.time.pretty; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.List; -import java.util.Locale; - -import static org.junit.Assert.assertEquals; - -public class PrettyTimeI18n_BG_Test { - - // Stores current locale so that it can be restored - private Locale locale; - - // Method setUp() is called automatically before every test method - @Before - public void setUp() throws Exception { - locale = Locale.getDefault(); - Locale.setDefault(new Locale("bg")); - } - - @Test - public void testCenturiesFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 века", t.format(3155692597470L * 3L)); - } - - @Test - public void testCenturiesAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 века", t.format(0)); - } - - @Test - public void testCenturySingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 1 век", t.format(0)); - } - - @Test - public void testDaysFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 дни", t.format(1000 * 60 * 60 * 24 * 3)); - } - - @Test - public void testDaysAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 дни", t.format(0)); - } - - @Test - public void testDaySingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 1 ден", t.format(0)); - } - - @Test - public void testDecadesAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(315569259747L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 десетилетия", t.format(0)); - } - - @Test - public void testDecadesFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 десетилетия", t.format(315569259747L * 3L)); - } - - @Test - public void testDecadeSingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 1 десетилетие", t.format(315569259747L)); - } - - @Test - public void testHoursFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 часа", t.format(1000 * 60 * 60 * 3)); - } - - @Test - public void testHoursAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 3), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 часа", t.format(0)); - } - - @Test - public void testHourSingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 1 час", t.format(0)); - } - - @Test - public void testRightNow() throws Exception { - PrettyTime t = new PrettyTime(); - assertEquals("в момента", t.format(LocalDateTime.now())); - } - - @Test - public void testMomentsAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(6000), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("току що", t.format(0)); - } - - @Test - public void testMinutesFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 12 минути", t.format(1000 * 60 * 12)); - } - - @Test - public void testMinutesAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 12), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 12 минути", t.format(0)); - } - - @Test - public void testMonthsFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 месеца", t.format((2629743830L * 3L))); - } - - @Test - public void testMonthsAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 месеца", t.format(0)); - } - - @Test - public void testMonthSingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 1 месец", t.format(0)); - } - - @Test - public void testWeeksFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 седмици", t.format(1000 * 60 * 60 * 24 * 7 * 3)); - } - - @Test - public void testWeeksAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 7 * 3), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 седмици", t.format(0)); - } - - @Test - public void testWeekSingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 7), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 1 седмица", t.format(0)); - } - - @Test - public void testYearsFromNow() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("след 3 години", t.format(2629743830L * 12L * 3L)); - } - - @Test - public void testYearsAgo() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 12L * 3L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 3 години", t.format(0)); - } - - @Test - public void testYearSingular() throws Exception { - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 12L), ZoneId.systemDefault()); - PrettyTime t = new PrettyTime(localDateTime); - assertEquals("преди 1 година", t.format(0)); - } - - @Test - public void testFormattingDurationListInThePast() throws Exception { - PrettyTime t = new PrettyTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38), ZoneId.systemDefault())); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneId.systemDefault()); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertEquals("преди 3 дни 15 часа 38 минути", t.format(timeUnitQuantities)); - } - - @Test - public void testFormattingDurationListInTheFuture() throws Exception { - PrettyTime t = new PrettyTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneId.systemDefault())); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 - + 1000 * 60 * 38), ZoneId.systemDefault()); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertEquals("след 3 дни 15 часа 38 минути", t.format(timeUnitQuantities)); - } - - // Method tearDown() is called automatically after every test method - @After - public void tearDown() throws Exception { - Locale.setDefault(locale); - } - -} +package org.xbib.time.pretty; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; + +public class PrettyTimeI18n_BG_Test { + + private Locale defaultLocale; + + private Locale locale; + + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("bg"); + Locale.setDefault(locale); + } + + @Test + public void testCenturiesFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 века", t.format(3155692597470L * 3L)); + } + + @Test + public void testCenturiesAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 века", t.format(0)); + } + + @Test + public void testCenturySingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 1 век", t.format(0)); + } + + @Test + public void testDaysFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 дни", t.format(1000 * 60 * 60 * 24 * 3)); + } + + @Test + public void testDaysAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 дни", t.format(0)); + } + + @Test + public void testDaySingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 1 ден", t.format(0)); + } + + @Test + public void testDecadesAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(315569259747L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 десетилетия", t.format(0)); + } + + @Test + public void testDecadesFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 десетилетия", t.format(315569259747L * 3L)); + } + + @Test + public void testDecadeSingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 1 десетилетие", t.format(315569259747L)); + } + + @Test + public void testHoursFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 часа", t.format(1000 * 60 * 60 * 3)); + } + + @Test + public void testHoursAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 3), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 часа", t.format(0)); + } + + @Test + public void testHourSingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 1 час", t.format(0)); + } + + @Test + public void testRightNow() throws Exception { + PrettyTime t = new PrettyTime(); + assertEquals("в момента", t.format(LocalDateTime.now())); + } + + @Test + public void testMomentsAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(6000), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("току що", t.format(0)); + } + + @Test + public void testMinutesFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 12 минути", t.format(1000 * 60 * 12)); + } + + @Test + public void testMinutesAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 12), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 12 минути", t.format(0)); + } + + @Test + public void testMonthsFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 месеца", t.format((2629743830L * 3L))); + } + + @Test + public void testMonthsAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 месеца", t.format(0)); + } + + @Test + public void testMonthSingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 1 месец", t.format(0)); + } + + @Test + public void testWeeksFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 седмици", t.format(1000 * 60 * 60 * 24 * 7 * 3)); + } + + @Test + public void testWeeksAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 7 * 3), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 седмици", t.format(0)); + } + + @Test + public void testWeekSingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 7), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 1 седмица", t.format(0)); + } + + @Test + public void testYearsFromNow() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("след 3 години", t.format(2629743830L * 12L * 3L)); + } + + @Test + public void testYearsAgo() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 12L * 3L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 3 години", t.format(0)); + } + + @Test + public void testYearSingular() throws Exception { + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(2629743830L * 12L), ZoneId.systemDefault()); + PrettyTime t = new PrettyTime(localDateTime); + assertEquals("преди 1 година", t.format(0)); + } + + @Test + public void testFormattingDurationListInThePast() throws Exception { + PrettyTime t = new PrettyTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38), ZoneId.systemDefault())); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneId.systemDefault()); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertEquals("преди 3 дни 15 часа 38 минути", t.format(timeUnitQuantities)); + } + + @Test + public void testFormattingDurationListInTheFuture() throws Exception { + PrettyTime t = new PrettyTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneId.systemDefault())); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + + 1000 * 60 * 38), ZoneId.systemDefault()); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertEquals("след 3 дни 15 часа 38 минути", t.format(timeUnitQuantities)); + } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } + +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CA_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CA_Test.java index c9f5df5..73d20f1 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CA_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CA_Test.java @@ -1,7 +1,7 @@ package org.xbib.time.pretty; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import java.time.Instant; @@ -15,16 +15,14 @@ import static org.junit.Assert.assertTrue; public class PrettyTimeI18n_CA_Test { - protected static Locale locale; + private Locale defaultLocale; - @BeforeClass - public static void setUp() throws Exception { - locale = Locale.getDefault(); - Locale.setDefault(new Locale("ca")); - } + private Locale locale; - @AfterClass - public static void tearDown() throws Exception { + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("ca"); Locale.setDefault(locale); } @@ -230,4 +228,8 @@ public class PrettyTimeI18n_CA_Test { assertEquals("vor 3 Jahrzehnten", t.format((0))); } + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CS_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CS_Test.java index 5baf377..90c24a1 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CS_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_CS_Test.java @@ -1,8 +1,8 @@ package org.xbib.time.pretty; -import org.junit.AfterClass; +import org.junit.After; import org.junit.Assert; -import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Test; import org.xbib.time.pretty.units.JustNow; import org.xbib.time.pretty.units.Month; @@ -17,16 +17,15 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_CS_Test { - private static Locale locale; - @BeforeClass - public static void setUp() throws Exception { - locale = Locale.getDefault(); - Locale.setDefault(new Locale("cs")); - } + private Locale defaultLocale; - @AfterClass - public static void tearDown() throws Exception { + private Locale locale; + + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("cs"); Locale.setDefault(locale); } @@ -213,7 +212,7 @@ public class PrettyTimeI18n_CS_Test { /** * Tests formatApproximateDuration and by proxy, formatDuration. * - * @throws Exception + * @throws Exception exception */ @Test public void testFormatApproximateDuration() throws Exception { @@ -224,4 +223,8 @@ public class PrettyTimeI18n_CS_Test { Assert.assertEquals("10 minutami", result); } + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_DA_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_DA_Test.java index 0febb08..87bac2b 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_DA_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_DA_Test.java @@ -1,155 +1,166 @@ -package org.xbib.time.pretty; - -import org.junit.Before; -import org.junit.Test; - -import java.time.LocalDateTime; -import java.util.Locale; - -import static org.junit.Assert.assertEquals; - -public class PrettyTimeI18n_DA_Test { - private Locale locale; - - @Before - public void setUp() throws Exception { - locale = new Locale("da"); - } - - @Test - public void testPrettyTime() { - PrettyTime p = new PrettyTime(locale); - assertEquals("straks", p.format(LocalDateTime.now())); - } - - @Test - public void testPrettyTimeCenturies() { - PrettyTime p = new PrettyTime(3155692597470L * 3L, locale); - assertEquals("3 århundreder siden", p.format(0)); - - p = new PrettyTime(0, locale); - assertEquals("3 århundreder fra nu", p.format(3155692597470L * 3L)); - } - - @Test - public void testCeilingInterval() throws Exception { - LocalDateTime then = LocalDateTime.of(2009, 5, 20, 0, 0); - LocalDateTime ref = LocalDateTime.of(2009, 6, 17, 0, 0); - PrettyTime t = new PrettyTime(ref, locale); - assertEquals("1 måned siden", t.format(then)); - } - - @Test - public void testRightNow() throws Exception { - PrettyTime t = new PrettyTime(locale); - assertEquals("straks", t.format(LocalDateTime.now())); - } - - @Test - public void testRightNowVariance() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("straks", t.format(600)); - } - - @Test - public void testMinutesFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("om 12 minutter", t.format(1000 * 60 * 12)); - } - - @Test - public void testHoursFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("om 3 timer", t.format(1000 * 60 * 60 * 3)); - } - - @Test - public void testDaysFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("om 3 dage", t.format(1000 * 60 * 60 * 24 * 3)); - } - - @Test - public void testWeeksFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("om 3 uger", t.format(1000 * 60 * 60 * 24 * 7 * 3)); - } - - @Test - public void testMonthsFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("om 3 måneder", t.format(2629743830L * 3L)); - } - - @Test - public void testYearsFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("om 3 år", t.format(2629743830L * 12L * 3L)); - } - - @Test - public void testDecadesFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("3 årtier fra nu", t.format(315569259747L * 3L)); - } - - @Test - public void testCenturiesFromNow() throws Exception { - PrettyTime t = new PrettyTime(0, locale); - assertEquals("3 århundreder fra nu", t.format(3155692597470L * 3L)); - } - - @Test - public void testMomentsAgo() throws Exception { - PrettyTime t = new PrettyTime(6000, locale); - assertEquals("et øjeblik siden", t.format(0)); - } - - @Test - public void testMinutesAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 12), locale); - assertEquals("12 minutter siden", t.format(0)); - } - - @Test - public void testHoursAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 60 * 3), locale); - assertEquals("3 timer siden", t.format(0)); - } - - @Test - public void testDaysAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 3), locale); - assertEquals("3 dage siden", t.format(0)); - } - - @Test - public void testWeeksAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 7 * 3), locale); - assertEquals("3 uger siden", t.format(0)); - } - - @Test - public void testMonthsAgo() throws Exception { - PrettyTime t = new PrettyTime((2629743830L * 3L), locale); - assertEquals("3 måneder siden", t.format(0)); - } - - @Test - public void testYearsAgo() throws Exception { - PrettyTime t = new PrettyTime((2629743830L * 12L * 3L), locale); - assertEquals("3 år siden", t.format(0)); - } - - @Test - public void testDecadesAgo() throws Exception { - PrettyTime t = new PrettyTime((315569259747L * 3L), locale); - assertEquals("3 årtier siden", t.format(0)); - } - - @Test - public void testCenturiesAgo() throws Exception { - PrettyTime t = new PrettyTime((3155692597470L * 3L), locale); - assertEquals("3 århundreder siden", t.format(0)); - } -} \ No newline at end of file +package org.xbib.time.pretty; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; + +public class PrettyTimeI18n_DA_Test { + + private Locale defaultLocale; + + private Locale locale; + + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("da"); + Locale.setDefault(locale); + } + + @Test + public void testPrettyTime() { + PrettyTime p = new PrettyTime(locale); + assertEquals("straks", p.format(LocalDateTime.now())); + } + + @Test + public void testPrettyTimeCenturies() { + PrettyTime p = new PrettyTime(3155692597470L * 3L, locale); + assertEquals("3 århundreder siden", p.format(0)); + + p = new PrettyTime(0, locale); + assertEquals("3 århundreder fra nu", p.format(3155692597470L * 3L)); + } + + @Test + public void testCeilingInterval() throws Exception { + LocalDateTime then = LocalDateTime.of(2009, 5, 20, 0, 0); + LocalDateTime ref = LocalDateTime.of(2009, 6, 17, 0, 0); + PrettyTime t = new PrettyTime(ref, locale); + assertEquals("1 måned siden", t.format(then)); + } + + @Test + public void testRightNow() throws Exception { + PrettyTime t = new PrettyTime(locale); + assertEquals("straks", t.format(LocalDateTime.now())); + } + + @Test + public void testRightNowVariance() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("straks", t.format(600)); + } + + @Test + public void testMinutesFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("om 12 minutter", t.format(1000 * 60 * 12)); + } + + @Test + public void testHoursFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("om 3 timer", t.format(1000 * 60 * 60 * 3)); + } + + @Test + public void testDaysFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("om 3 dage", t.format(1000 * 60 * 60 * 24 * 3)); + } + + @Test + public void testWeeksFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("om 3 uger", t.format(1000 * 60 * 60 * 24 * 7 * 3)); + } + + @Test + public void testMonthsFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("om 3 måneder", t.format(2629743830L * 3L)); + } + + @Test + public void testYearsFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("om 3 år", t.format(2629743830L * 12L * 3L)); + } + + @Test + public void testDecadesFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("3 årtier fra nu", t.format(315569259747L * 3L)); + } + + @Test + public void testCenturiesFromNow() throws Exception { + PrettyTime t = new PrettyTime(0, locale); + assertEquals("3 århundreder fra nu", t.format(3155692597470L * 3L)); + } + + @Test + public void testMomentsAgo() throws Exception { + PrettyTime t = new PrettyTime(6000, locale); + assertEquals("et øjeblik siden", t.format(0)); + } + + @Test + public void testMinutesAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 12), locale); + assertEquals("12 minutter siden", t.format(0)); + } + + @Test + public void testHoursAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 60 * 3), locale); + assertEquals("3 timer siden", t.format(0)); + } + + @Test + public void testDaysAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 3), locale); + assertEquals("3 dage siden", t.format(0)); + } + + @Test + public void testWeeksAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 7 * 3), locale); + assertEquals("3 uger siden", t.format(0)); + } + + @Test + public void testMonthsAgo() throws Exception { + PrettyTime t = new PrettyTime((2629743830L * 3L), locale); + assertEquals("3 måneder siden", t.format(0)); + } + + @Test + public void testYearsAgo() throws Exception { + PrettyTime t = new PrettyTime((2629743830L * 12L * 3L), locale); + assertEquals("3 år siden", t.format(0)); + } + + @Test + public void testDecadesAgo() throws Exception { + PrettyTime t = new PrettyTime((315569259747L * 3L), locale); + assertEquals("3 årtier siden", t.format(0)); + } + + @Test + public void testCenturiesAgo() throws Exception { + PrettyTime t = new PrettyTime((3155692597470L * 3L), locale); + assertEquals("3 århundreder siden", t.format(0)); + } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_ET_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_ET_Test.java index 546f3dd..0eaf932 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_ET_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_ET_Test.java @@ -1,5 +1,6 @@ package org.xbib.time.pretty; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.xbib.time.pretty.units.JustNow; @@ -14,11 +15,16 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_ET_Test { + + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("et"); + Locale.setDefault(locale); } @Test @@ -333,4 +339,8 @@ public class PrettyTimeI18n_ET_Test { return t; } + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FA_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FA_Test.java index dc96683..a476989 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FA_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FA_Test.java @@ -1,239 +1,238 @@ -package org.xbib.time.pretty; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.List; -import java.util.Locale; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class PrettyTimeI18n_FA_Test { - - // Stores current locale so that it can be restored - private Locale locale; - - // Method setUp() is called automatically before every test method - @Before - public void setUp() throws Exception { - locale = new Locale("fa"); - Locale.setDefault(locale); - } - - @Test - public void testCeilingInterval() throws Exception { - LocalDateTime then = LocalDateTime.of(2012, 5, 20, 0, 0); - LocalDateTime ref = LocalDateTime.of(2012, 6, 17, 0, 0); - PrettyTime t = new PrettyTime(ref); - assertEquals("1 ماه پیش", t.format(then)); - } - - @Test - public void testRightNow() throws Exception { - PrettyTime t = new PrettyTime(); - assertEquals("چند لحظه دیگر", t.format(LocalDateTime.now())); - } - - @Test - public void testRightNowVariance() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("چند لحظه دیگر", t.format((600))); - } - - @Test - public void testMinutesFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("12 دقیقه دیگر", t.format((1000 * 60 * 12))); - } - - @Test - public void testHoursFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 ساعت دیگر", t.format((1000 * 60 * 60 * 3))); - } - - @Test - public void testDaysFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 روز دیگر", t.format((1000 * 60 * 60 * 24 * 3))); - } - - @Test - public void testWeeksFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 هفته دیگر", t.format((1000 * 60 * 60 * 24 * 7 * 3))); - } - - @Test - public void testMonthsFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 ماه دیگر", t.format((2629743830L * 3L))); - } - - @Test - public void testYearsFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 سال دیگر", t.format((2629743830L * 12L * 3L))); - } - - @Test - public void testDecadesFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 دهه دیگر", t.format((315569259747L * 3L))); - } - - @Test - public void testCenturiesFromNow() throws Exception { - PrettyTime t = new PrettyTime(0); - assertEquals("3 قرن دیگر", t.format((3155692597470L * 3L))); - } - - /* - * Past - */ - @Test - public void testMomentsAgo() throws Exception { - PrettyTime t = new PrettyTime(6000); - assertEquals("چند لحظه پیش", t.format((0))); - } - - @Test - public void testMinutesAgo() throws Exception { - PrettyTime t = new PrettyTime(1000 * 60 * 12); - assertEquals("12 دقیقه پیش", t.format((0))); - } - - @Test - public void testHoursAgo() throws Exception { - PrettyTime t = new PrettyTime(1000 * 60 * 60 * 3); - assertEquals("3 ساعت پیش", t.format((0))); - } - - @Test - public void testDaysAgo() throws Exception { - PrettyTime t = new PrettyTime(1000 * 60 * 60 * 24 * 3); - assertEquals("3 روز پیش", t.format((0))); - } - - @Test - public void testWeeksAgo() throws Exception { - PrettyTime t = new PrettyTime(1000 * 60 * 60 * 24 * 7 * 3); - assertEquals("3 هفته پیش", t.format((0))); - } - - @Test - public void testMonthsAgo() throws Exception { - PrettyTime t = new PrettyTime(2629743830L * 3L); - assertEquals("3 ماه پیش", t.format((0))); - } - - @Test - public void testCustomFormat() throws Exception { - PrettyTime t = new PrettyTime(0); - TimeUnit unit = new TimeUnit() { - @Override - public long getMaxQuantity() { - return 0; - } - - @Override - public long getMillisPerUnit() { - return 5000; - } - }; - t.clearUnits(); - t.registerUnit(unit, new SimpleTimeFormat() - .setSingularName("tick").setPluralName("ticks") - .setPattern("%n %u").setRoundingTolerance(20) - .setFutureSuffix("... RUN!") - .setFuturePrefix("self destruct in: ").setPastPrefix("self destruct was: ").setPastSuffix( - " ago...")); - - assertEquals("self destruct in: 5 ticks ... RUN!", t.format((25000))); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(25000), ZoneId.systemDefault()); - t.setReference(localDateTime); - assertEquals("self destruct was: 5 ticks ago...", t.format((0))); - } - - @Test - public void testYearsAgo() throws Exception { - PrettyTime t = new PrettyTime(2629743830L * 12L * 3L); - assertEquals("3 سال پیش", t.format((0))); - } - - @Test - public void testDecadesAgo() throws Exception { - PrettyTime t = new PrettyTime(315569259747L * 3L); - assertEquals("3 دهه پیش", t.format((0))); - } - - @Test - public void testCenturiesAgo() throws Exception { - PrettyTime t = new PrettyTime(3155692597470L * 3L); - assertEquals("3 قرن پیش", t.format((0))); - } - - @Test - public void testWithinTwoHoursRounding() throws Exception { - PrettyTime t = new PrettyTime(); - LocalDateTime localDateTime = LocalDateTime.now().minusSeconds(6543); - assertEquals("2 ساعت پیش", t.format(localDateTime)); - } - - @Test - public void testPreciseInTheFuture() throws Exception { - PrettyTime t = new PrettyTime(); - LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(10 * 60 + 5 * 60 * 60); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertTrue(timeUnitQuantities.size() >= 2); // might be more because of milliseconds between date capturing and result - // calculation - assertEquals(5, timeUnitQuantities.get(0).getQuantity()); - assertEquals(10, timeUnitQuantities.get(1).getQuantity()); - } - - @Test - public void testPreciseInThePast() throws Exception { - PrettyTime t = new PrettyTime(); - LocalDateTime localDateTime = LocalDateTime.now().minusSeconds(10 * 60 + 5 * 60 * 60); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertTrue(timeUnitQuantities.size() >= 2); // might be more because of milliseconds between date capturing and result - // calculation - assertEquals(-5, timeUnitQuantities.get(0).getQuantity()); - assertTrue(-10 == timeUnitQuantities.get(1).getQuantity() || -9 == timeUnitQuantities.get(1).getQuantity()); - } - - @Test - public void testFormattingDurationListInThePast() throws Exception { - PrettyTime t = new PrettyTime(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertEquals("3 روز 15 ساعت 38 دقیقه پیش", t.format(timeUnitQuantities)); - } - - @Test - public void testFormattingDurationListInTheFuture() throws Exception { - PrettyTime t = new PrettyTime(0); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 - + 1000 * 60 * 38), ZoneId.systemDefault()); - List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); - assertEquals("3 روز 15 ساعت 38 دقیقه دیگر", t.format(timeUnitQuantities)); - } - - @Test - public void testSetLocale() throws Exception { - PrettyTime t = new PrettyTime(315569259747L * 3L); - assertEquals("3 دهه پیش", t.format((0))); - } - - // Method tearDown() is called automatically after every test method - @After - public void tearDown() throws Exception { - Locale.setDefault(locale); - } - -} +package org.xbib.time.pretty; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PrettyTimeI18n_FA_Test { + + private Locale defaultLocale; + + private Locale locale; + + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("fa"); + Locale.setDefault(locale); + } + + @Test + public void testCeilingInterval() throws Exception { + LocalDateTime then = LocalDateTime.of(2012, 5, 20, 0, 0); + LocalDateTime ref = LocalDateTime.of(2012, 6, 17, 0, 0); + PrettyTime t = new PrettyTime(ref); + assertEquals("1 ماه پیش", t.format(then)); + } + + @Test + public void testRightNow() throws Exception { + PrettyTime t = new PrettyTime(); + assertEquals("چند لحظه دیگر", t.format(LocalDateTime.now())); + } + + @Test + public void testRightNowVariance() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("چند لحظه دیگر", t.format((600))); + } + + @Test + public void testMinutesFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("12 دقیقه دیگر", t.format((1000 * 60 * 12))); + } + + @Test + public void testHoursFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 ساعت دیگر", t.format((1000 * 60 * 60 * 3))); + } + + @Test + public void testDaysFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 روز دیگر", t.format((1000 * 60 * 60 * 24 * 3))); + } + + @Test + public void testWeeksFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 هفته دیگر", t.format((1000 * 60 * 60 * 24 * 7 * 3))); + } + + @Test + public void testMonthsFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 ماه دیگر", t.format((2629743830L * 3L))); + } + + @Test + public void testYearsFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 سال دیگر", t.format((2629743830L * 12L * 3L))); + } + + @Test + public void testDecadesFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 دهه دیگر", t.format((315569259747L * 3L))); + } + + @Test + public void testCenturiesFromNow() throws Exception { + PrettyTime t = new PrettyTime(0); + assertEquals("3 قرن دیگر", t.format((3155692597470L * 3L))); + } + + /* + * Past + */ + @Test + public void testMomentsAgo() throws Exception { + PrettyTime t = new PrettyTime(6000); + assertEquals("چند لحظه پیش", t.format((0))); + } + + @Test + public void testMinutesAgo() throws Exception { + PrettyTime t = new PrettyTime(1000 * 60 * 12); + assertEquals("12 دقیقه پیش", t.format((0))); + } + + @Test + public void testHoursAgo() throws Exception { + PrettyTime t = new PrettyTime(1000 * 60 * 60 * 3); + assertEquals("3 ساعت پیش", t.format((0))); + } + + @Test + public void testDaysAgo() throws Exception { + PrettyTime t = new PrettyTime(1000 * 60 * 60 * 24 * 3); + assertEquals("3 روز پیش", t.format((0))); + } + + @Test + public void testWeeksAgo() throws Exception { + PrettyTime t = new PrettyTime(1000 * 60 * 60 * 24 * 7 * 3); + assertEquals("3 هفته پیش", t.format((0))); + } + + @Test + public void testMonthsAgo() throws Exception { + PrettyTime t = new PrettyTime(2629743830L * 3L); + assertEquals("3 ماه پیش", t.format((0))); + } + + @Test + public void testCustomFormat() throws Exception { + PrettyTime t = new PrettyTime(0); + TimeUnit unit = new TimeUnit() { + @Override + public long getMaxQuantity() { + return 0; + } + + @Override + public long getMillisPerUnit() { + return 5000; + } + }; + t.clearUnits(); + t.registerUnit(unit, new SimpleTimeFormat() + .setSingularName("tick").setPluralName("ticks") + .setPattern("%n %u").setRoundingTolerance(20) + .setFutureSuffix("... RUN!") + .setFuturePrefix("self destruct in: ").setPastPrefix("self destruct was: ").setPastSuffix( + " ago...")); + + assertEquals("self destruct in: 5 ticks ... RUN!", t.format((25000))); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(25000), ZoneId.systemDefault()); + t.setReference(localDateTime); + assertEquals("self destruct was: 5 ticks ago...", t.format((0))); + } + + @Test + public void testYearsAgo() throws Exception { + PrettyTime t = new PrettyTime(2629743830L * 12L * 3L); + assertEquals("3 سال پیش", t.format((0))); + } + + @Test + public void testDecadesAgo() throws Exception { + PrettyTime t = new PrettyTime(315569259747L * 3L); + assertEquals("3 دهه پیش", t.format((0))); + } + + @Test + public void testCenturiesAgo() throws Exception { + PrettyTime t = new PrettyTime(3155692597470L * 3L); + assertEquals("3 قرن پیش", t.format((0))); + } + + @Test + public void testWithinTwoHoursRounding() throws Exception { + PrettyTime t = new PrettyTime(); + LocalDateTime localDateTime = LocalDateTime.now().minusSeconds(6543); + assertEquals("2 ساعت پیش", t.format(localDateTime)); + } + + @Test + public void testPreciseInTheFuture() throws Exception { + PrettyTime t = new PrettyTime(); + LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(10 * 60 + 5 * 60 * 60); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertTrue(timeUnitQuantities.size() >= 2); // might be more because of milliseconds between date capturing and result + // calculation + assertEquals(5, timeUnitQuantities.get(0).getQuantity()); + assertEquals(10, timeUnitQuantities.get(1).getQuantity()); + } + + @Test + public void testPreciseInThePast() throws Exception { + PrettyTime t = new PrettyTime(); + LocalDateTime localDateTime = LocalDateTime.now().minusSeconds(10 * 60 + 5 * 60 * 60); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertTrue(timeUnitQuantities.size() >= 2); // might be more because of milliseconds between date capturing and result + // calculation + assertEquals(-5, timeUnitQuantities.get(0).getQuantity()); + assertTrue(-10 == timeUnitQuantities.get(1).getQuantity() || -9 == timeUnitQuantities.get(1).getQuantity()); + } + + @Test + public void testFormattingDurationListInThePast() throws Exception { + PrettyTime t = new PrettyTime(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + 1000 * 60 * 38); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertEquals("3 روز 15 ساعت 38 دقیقه پیش", t.format(timeUnitQuantities)); + } + + @Test + public void testFormattingDurationListInTheFuture() throws Exception { + PrettyTime t = new PrettyTime(0); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1000 * 60 * 60 * 24 * 3 + 1000 * 60 * 60 * 15 + + 1000 * 60 * 38), ZoneId.systemDefault()); + List timeUnitQuantities = t.calculatePreciseDuration(localDateTime); + assertEquals("3 روز 15 ساعت 38 دقیقه دیگر", t.format(timeUnitQuantities)); + } + + @Test + public void testSetLocale() throws Exception { + PrettyTime t = new PrettyTime(315569259747L * 3L); + assertEquals("3 دهه پیش", t.format((0))); + } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FI_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FI_Test.java index 7fd0d44..f708a24 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FI_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FI_Test.java @@ -1,5 +1,6 @@ package org.xbib.time.pretty; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.xbib.time.pretty.units.JustNow; @@ -14,11 +15,16 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_FI_Test { + + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("fi"); + Locale.setDefault(locale); } @Test @@ -333,4 +339,9 @@ public class PrettyTimeI18n_FI_Test { } return t; } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FR_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FR_Test.java index 73a5927..cdfe3c9 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FR_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_FR_Test.java @@ -12,37 +12,34 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -/** - * All the tests for PrettyTime. - */ public class PrettyTimeI18n_FR_Test { - // Stores current locale so that it can be restored + private Locale defaultLocale; + private Locale locale; - // Method setUp() is called automatically before every test method @Before public void setUp() throws Exception { - locale = Locale.getDefault(); + defaultLocale = Locale.getDefault(); + locale = Locale.FRENCH; + Locale.setDefault(locale); } @Test public void testPrettyTimeFRENCH() { // The FRENCH resource bundle should be used - PrettyTime p = new PrettyTime(Locale.FRENCH); + PrettyTime p = new PrettyTime(locale); assertEquals("à l'instant", p.format(LocalDateTime.now())); } @Test public void testPrettyTimeFRENCHCenturies() { - PrettyTime p = new PrettyTime((3155692597470L * 3L), Locale.FRENCH); + PrettyTime p = new PrettyTime((3155692597470L * 3L), locale); assertEquals(p.format(0), "il y a 3 siècles"); } @Test public void testPrettyTimeViaDefaultLocaleFRENCH() { - // The FRENCH resource bundle should be used - Locale.setDefault(Locale.FRENCH); PrettyTime p = new PrettyTime(); assertEquals(p.format(LocalDateTime.now()), "à l'instant"); } @@ -50,7 +47,7 @@ public class PrettyTimeI18n_FR_Test { @Test public void testPrettyTimeFRENCHLocale() { long t = 1L; - PrettyTime p = new PrettyTime((0), Locale.FRENCH); + PrettyTime p = new PrettyTime((0), locale); while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) { LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); assertTrue(p.format(localDateTime).startsWith("dans") || p.format(localDateTime).startsWith("à l'instant")); @@ -58,10 +55,9 @@ public class PrettyTimeI18n_FR_Test { } } - // Method tearDown() is called automatically after every test method @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_IT_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_IT_Test.java index 24df2ff..2e96182 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_IT_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_IT_Test.java @@ -1,5 +1,6 @@ package org.xbib.time.pretty; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.xbib.time.pretty.units.JustNow; @@ -14,11 +15,16 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_IT_Test { + + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("it"); + Locale.setDefault(locale); } @Test @@ -333,4 +339,9 @@ public class PrettyTimeI18n_IT_Test { } return t; } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_KO_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_KO_Test.java index 8e1607e..e5d51cc 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_KO_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_KO_Test.java @@ -15,13 +15,15 @@ import static org.junit.Assert.assertTrue; public class PrettyTimeI18n_KO_Test { + private Locale defaultLocale; + private Locale locale; - // Method setUp() is called automatically before every test method @Before public void setUp() throws Exception { - locale = Locale.getDefault(); - Locale.setDefault(Locale.KOREA); + defaultLocale = Locale.getDefault(); + locale = Locale.KOREA; + Locale.setDefault(locale); } @Test @@ -227,10 +229,8 @@ public class PrettyTimeI18n_KO_Test { assertEquals("vor 3 Jahrzehnten", t.format((0))); } - // Method tearDown() is called automatically after every test method @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } - } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NL_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NL_Test.java index 86f19bc..b25f2d5 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NL_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NL_Test.java @@ -1,5 +1,6 @@ package org.xbib.time.pretty; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -11,11 +12,16 @@ import java.util.Locale; import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_NL_Test { + + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("nl"); + Locale.setDefault(locale); } @Test @@ -159,4 +165,9 @@ public class PrettyTimeI18n_NL_Test { PrettyTime t = new PrettyTime((3155692597470L * 3L), locale); assertEquals("3 eeuwen geleden", t.format((0))); } -} \ No newline at end of file + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NO_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NO_Test.java index 364e9fe..68fd08e 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NO_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_NO_Test.java @@ -1,163 +1,173 @@ -package org.xbib.time.pretty; - -import org.junit.Before; -import org.junit.Test; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Locale; - -import static org.junit.Assert.assertEquals; - -public class PrettyTimeI18n_NO_Test { - - private Locale locale; - - @Before - public void setUp() throws Exception { - locale = new Locale("no"); - } - - @Test - public void testPrettyTime() { - PrettyTime p = new PrettyTime(locale); - assertEquals("straks", p.format(LocalDateTime.now())); - } - - @Test - public void testPrettyTimeCenturies() { - PrettyTime p = new PrettyTime((3155692597470L * 3L), locale); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - assertEquals("3 århundre siden", p.format(localDateTime)); - - p = new PrettyTime(0, locale); - localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L * 3L), ZoneId.systemDefault()); - assertEquals("3 århundre fra nå", p.format(localDateTime)); - } - - @Test - public void testCeilingInterval() throws Exception { - LocalDateTime then = LocalDateTime.of(2009, 5, 20, 0, 0); - LocalDateTime ref = LocalDateTime.of(2009, 6, 17, 0, 0); - PrettyTime t = new PrettyTime(ref, locale); - assertEquals("1 måned siden", t.format(then)); - } - - @Test - public void testRightNow() throws Exception { - PrettyTime t = new PrettyTime(locale); - assertEquals("straks", t.format(LocalDateTime.now())); - } - - @Test - public void testRightNowVariance() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("straks", t.format((600))); - } - - @Test - public void testMinutesFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("om 12 minutter", t.format((1000 * 60 * 12))); - } - - @Test - public void testHoursFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("om 3 timer", t.format((1000 * 60 * 60 * 3))); - } - - @Test - public void testDaysFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("om 3 dager", t.format((1000 * 60 * 60 * 24 * 3))); - } - - @Test - public void testWeeksFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("om 3 uker", t.format((1000 * 60 * 60 * 24 * 7 * 3))); - } - - @Test - public void testMonthsFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("om 3 måneder", t.format((2629743830L * 3L))); - } - - @Test - public void testYearsFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("om 3 år", t.format((2629743830L * 12L * 3L))); - } - - @Test - public void testDecadesFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("3 tiår fra nå", t.format((315569259747L * 3L))); - } - - @Test - public void testCenturiesFromNow() throws Exception { - PrettyTime t = new PrettyTime((0), locale); - assertEquals("3 århundre fra nå", t.format((3155692597470L * 3L))); - } - - /* - * Past - */ - @Test - public void testMomentsAgo() throws Exception { - PrettyTime t = new PrettyTime((6000), locale); - assertEquals("et øyeblikk siden", t.format((0))); - } - - @Test - public void testMinutesAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 12), locale); - assertEquals("12 minutter siden", t.format((0))); - } - - @Test - public void testHoursAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 60 * 3), locale); - assertEquals("3 timer siden", t.format((0))); - } - - @Test - public void testDaysAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 3), locale); - assertEquals("3 dager siden", t.format((0))); - } - - @Test - public void testWeeksAgo() throws Exception { - PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 7 * 3), locale); - assertEquals("3 uker siden", t.format((0))); - } - - @Test - public void testMonthsAgo() throws Exception { - PrettyTime t = new PrettyTime((2629743830L * 3L), locale); - assertEquals("3 måneder siden", t.format((0))); - } - - @Test - public void testYearsAgo() throws Exception { - PrettyTime t = new PrettyTime((2629743830L * 12L * 3L), locale); - assertEquals("3 år siden", t.format((0))); - } - - @Test - public void testDecadesAgo() throws Exception { - PrettyTime t = new PrettyTime((315569259747L * 3L), locale); - assertEquals("3 tiår siden", t.format((0))); - } - - @Test - public void testCenturiesAgo() throws Exception { - PrettyTime t = new PrettyTime((3155692597470L * 3L), locale); - assertEquals("3 århundre siden", t.format((0))); - } -} \ No newline at end of file +package org.xbib.time.pretty; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; + +public class PrettyTimeI18n_NO_Test { + + private Locale defaultLocale; + + private Locale locale; + + @Before + public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); + locale = new Locale("no"); + Locale.setDefault(locale); + } + + @Test + public void testPrettyTime() { + PrettyTime p = new PrettyTime(locale); + assertEquals("straks", p.format(LocalDateTime.now())); + } + + @Test + public void testPrettyTimeCenturies() { + PrettyTime p = new PrettyTime((3155692597470L * 3L), locale); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + assertEquals("3 århundre siden", p.format(localDateTime)); + + p = new PrettyTime(0, locale); + localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(3155692597470L * 3L), ZoneId.systemDefault()); + assertEquals("3 århundre fra nå", p.format(localDateTime)); + } + + @Test + public void testCeilingInterval() throws Exception { + LocalDateTime then = LocalDateTime.of(2009, 5, 20, 0, 0); + LocalDateTime ref = LocalDateTime.of(2009, 6, 17, 0, 0); + PrettyTime t = new PrettyTime(ref, locale); + assertEquals("1 måned siden", t.format(then)); + } + + @Test + public void testRightNow() throws Exception { + PrettyTime t = new PrettyTime(locale); + assertEquals("straks", t.format(LocalDateTime.now())); + } + + @Test + public void testRightNowVariance() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("straks", t.format((600))); + } + + @Test + public void testMinutesFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("om 12 minutter", t.format((1000 * 60 * 12))); + } + + @Test + public void testHoursFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("om 3 timer", t.format((1000 * 60 * 60 * 3))); + } + + @Test + public void testDaysFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("om 3 dager", t.format((1000 * 60 * 60 * 24 * 3))); + } + + @Test + public void testWeeksFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("om 3 uker", t.format((1000 * 60 * 60 * 24 * 7 * 3))); + } + + @Test + public void testMonthsFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("om 3 måneder", t.format((2629743830L * 3L))); + } + + @Test + public void testYearsFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("om 3 år", t.format((2629743830L * 12L * 3L))); + } + + @Test + public void testDecadesFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("3 tiår fra nå", t.format((315569259747L * 3L))); + } + + @Test + public void testCenturiesFromNow() throws Exception { + PrettyTime t = new PrettyTime((0), locale); + assertEquals("3 århundre fra nå", t.format((3155692597470L * 3L))); + } + + /* + * Past + */ + @Test + public void testMomentsAgo() throws Exception { + PrettyTime t = new PrettyTime((6000), locale); + assertEquals("et øyeblikk siden", t.format((0))); + } + + @Test + public void testMinutesAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 12), locale); + assertEquals("12 minutter siden", t.format((0))); + } + + @Test + public void testHoursAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 60 * 3), locale); + assertEquals("3 timer siden", t.format((0))); + } + + @Test + public void testDaysAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 3), locale); + assertEquals("3 dager siden", t.format((0))); + } + + @Test + public void testWeeksAgo() throws Exception { + PrettyTime t = new PrettyTime((1000 * 60 * 60 * 24 * 7 * 3), locale); + assertEquals("3 uker siden", t.format((0))); + } + + @Test + public void testMonthsAgo() throws Exception { + PrettyTime t = new PrettyTime((2629743830L * 3L), locale); + assertEquals("3 måneder siden", t.format((0))); + } + + @Test + public void testYearsAgo() throws Exception { + PrettyTime t = new PrettyTime((2629743830L * 12L * 3L), locale); + assertEquals("3 år siden", t.format((0))); + } + + @Test + public void testDecadesAgo() throws Exception { + PrettyTime t = new PrettyTime((315569259747L * 3L), locale); + assertEquals("3 tiår siden", t.format((0))); + } + + @Test + public void testCenturiesAgo() throws Exception { + PrettyTime t = new PrettyTime((3155692597470L * 3L), locale); + assertEquals("3 århundre siden", t.format((0))); + } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_RU_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_RU_Test.java index 5a912ac..aa595cc 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_RU_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_RU_Test.java @@ -13,10 +13,13 @@ import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_RU_Test { + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("ru"); Locale.setDefault(locale); } @@ -163,9 +166,8 @@ public class PrettyTimeI18n_RU_Test { assertEquals("3 века назад", t.format((0))); } - @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_SV_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_SV_Test.java index a833e4d..0a5d0a1 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_SV_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_SV_Test.java @@ -1,5 +1,6 @@ package org.xbib.time.pretty; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -12,11 +13,15 @@ import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_SV_Test { + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("sv"); + Locale.setDefault(locale); } @Test @@ -161,4 +166,9 @@ public class PrettyTimeI18n_SV_Test { PrettyTime t = new PrettyTime((3155692597470L * 3L), locale); assertEquals("3 århundraden sedan", t.format((0))); } + + @After + public void tearDown() throws Exception { + Locale.setDefault(defaultLocale); + } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_Test.java index b9bfe7b..6726925 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_Test.java @@ -1,130 +1,128 @@ -package org.xbib.time.pretty; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Locale; - -import static org.junit.Assert.assertEquals; - -public class PrettyTimeI18n_Test { - - // Stores current locale so that it can be restored - private Locale locale; - - // Method setUp() is called automatically before every test method - @Before - public void setUp() throws Exception { - locale = Locale.getDefault(); - } - - @Test - public void testPrettyTimeDefault() { - // The default resource bundle should be used - PrettyTime p = new PrettyTime(0, Locale.ROOT); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); - assertEquals("moments from now", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeGerman() { - // The German resource bundle should be used - PrettyTime p = new PrettyTime(Locale.GERMAN); - LocalDateTime ref = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - p.setReference(ref); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); - assertEquals("Jetzt", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeSpanish() { - // The Spanish resource bundle should be used - PrettyTime p = new PrettyTime(new Locale("es")); - assertEquals("en un instante", p.format(LocalDateTime.now())); - } - - @Test - public void testPrettyTimeDefaultCenturies() { - // The default resource bundle should be used - PrettyTime p = new PrettyTime((3155692597470L * 3L), Locale.ROOT); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - assertEquals("3 centuries ago", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeGermanCenturies() { - // The default resource bundle should be used - PrettyTime p = new PrettyTime((3155692597470L * 3L), Locale.GERMAN); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - assertEquals("vor 3 Jahrhunderten", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeViaDefaultLocaleDefault() { - // The default resource bundle should be used - Locale.setDefault(Locale.ROOT); - PrettyTime p = new PrettyTime((0)); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); - assertEquals("moments from now", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeViaDefaultLocaleGerman() { - // The German resource bundle should be used - Locale.setDefault(Locale.GERMAN); - PrettyTime p = new PrettyTime((0)); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); - assertEquals("Jetzt", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeViaDefaultLocaleDefaultCenturies() { - // The default resource bundle should be used - Locale.setDefault(Locale.ROOT); - PrettyTime p = new PrettyTime((3155692597470L * 3L)); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - assertEquals("3 centuries ago", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeViaDefaultLocaleGermanCenturies() { - // The default resource bundle should be used - Locale.setDefault(Locale.GERMAN); - PrettyTime p = new PrettyTime((3155692597470L * 3L)); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - assertEquals("vor 3 Jahrhunderten", p.format(localDateTime)); - } - - @Test - public void testPrettyTimeRootLocale() { - long t = 1L; - PrettyTime p = new PrettyTime(0, Locale.ROOT); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) { - assertEquals(p.format(localDateTime).endsWith("now"), true); - t *= 2L; - } - } - - @Test - public void testPrettyTimeGermanLocale() { - long t = 1L; - PrettyTime p = new PrettyTime(0, Locale.GERMAN); - LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); - while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) { - assertEquals(p.format(localDateTime).startsWith("in") || p.format(localDateTime).startsWith("Jetzt"), true); - t *= 2L; - } - } - - // Method tearDown() is called automatically after every test method - @After - public void tearDown() throws Exception { - Locale.setDefault(locale); - } - -} +package org.xbib.time.pretty; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PrettyTimeI18n_Test { + + private Locale locale; + + @Before + public void setUp() throws Exception { + locale = Locale.getDefault(); + } + + @Test + public void testPrettyTimeDefault() { + // The default resource bundle should be used + PrettyTime p = new PrettyTime(0, Locale.ROOT); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); + assertEquals("moments from now", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeGerman() { + // The German resource bundle should be used + PrettyTime p = new PrettyTime(Locale.GERMAN); + LocalDateTime ref = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + p.setReference(ref); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); + assertEquals("Jetzt", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeSpanish() { + // The Spanish resource bundle should be used + PrettyTime p = new PrettyTime(new Locale("es")); + assertEquals("en un instante", p.format(LocalDateTime.now())); + } + + @Test + public void testPrettyTimeDefaultCenturies() { + // The default resource bundle should be used + PrettyTime p = new PrettyTime((3155692597470L * 3L), Locale.ROOT); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + assertEquals("3 centuries ago", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeGermanCenturies() { + // The default resource bundle should be used + PrettyTime p = new PrettyTime((3155692597470L * 3L), Locale.GERMAN); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + assertEquals("vor 3 Jahrhunderten", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeViaDefaultLocaleDefault() { + // The default resource bundle should be used + Locale.setDefault(Locale.ROOT); + PrettyTime p = new PrettyTime((0)); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); + assertEquals("moments from now", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeViaDefaultLocaleGerman() { + // The German resource bundle should be used + Locale.setDefault(Locale.GERMAN); + PrettyTime p = new PrettyTime((0)); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(1), ZoneId.systemDefault()); + assertEquals("Jetzt", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeViaDefaultLocaleDefaultCenturies() { + // The default resource bundle should be used + Locale.setDefault(Locale.ROOT); + PrettyTime p = new PrettyTime((3155692597470L * 3L)); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + assertEquals("3 centuries ago", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeViaDefaultLocaleGermanCenturies() { + // The default resource bundle should be used + Locale.setDefault(Locale.GERMAN); + PrettyTime p = new PrettyTime((3155692597470L * 3L)); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + assertEquals("vor 3 Jahrhunderten", p.format(localDateTime)); + } + + @Test + public void testPrettyTimeRootLocale() { + long t = 1L; + PrettyTime p = new PrettyTime(0, Locale.ROOT); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) { + assertTrue(p.format(localDateTime).endsWith("now")); + t *= 2L; + } + } + + @Test + public void testPrettyTimeGermanLocale() { + long t = 1L; + PrettyTime p = new PrettyTime(0, Locale.GERMAN); + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) { + assertTrue(p.format(localDateTime).startsWith("in") || p.format(localDateTime).startsWith("Jetzt")); + t *= 2L; + } + } + + @After + public void tearDown() throws Exception { + Locale.setDefault(locale); + } + +} diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_UA_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_UA_Test.java index babd14b..0292d6d 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_UA_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_UA_Test.java @@ -13,10 +13,13 @@ import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_UA_Test { + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("ua"); Locale.setDefault(locale); } @@ -189,6 +192,6 @@ public class PrettyTimeI18n_UA_Test { @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_hi_IN_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_hi_IN_Test.java index 3629bd1..849bcad 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_hi_IN_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_hi_IN_Test.java @@ -15,10 +15,13 @@ import static org.junit.Assert.assertTrue; public class PrettyTimeI18n_hi_IN_Test { + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("hi", "IN"); Locale.setDefault(locale); } @@ -27,7 +30,7 @@ public class PrettyTimeI18n_hi_IN_Test { public void testLocaleISOCorrectness() { assertEquals("hi", this.locale.getLanguage()); assertEquals("IN", this.locale.getCountry()); - assertEquals("हिंदी", this.locale.getDisplayLanguage()); + assertEquals("हिन्दी", this.locale.getDisplayLanguage()); assertEquals("भारत", this.locale.getDisplayCountry()); } @@ -243,6 +246,6 @@ public class PrettyTimeI18n_hi_IN_Test { @After public void tearDown() throws Exception { - Locale.setDefault(Locale.ENGLISH); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_in_ID_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_in_ID_Test.java index 0f13192..624f820 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_in_ID_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_in_ID_Test.java @@ -15,10 +15,13 @@ import static org.junit.Assert.assertTrue; public class PrettyTimeI18n_in_ID_Test { + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = new Locale("in", "ID"); Locale.setDefault(locale); } @@ -242,6 +245,6 @@ public class PrettyTimeI18n_in_ID_Test { @After public void tearDown() throws Exception { - Locale.setDefault(Locale.ENGLISH); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_zh_TW_Test.java b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_zh_TW_Test.java index dd9e14b..176c215 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_zh_TW_Test.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeI18n_zh_TW_Test.java @@ -11,12 +11,15 @@ import static org.junit.Assert.assertEquals; public class PrettyTimeI18n_zh_TW_Test { + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { + defaultLocale = Locale.getDefault(); locale = Locale.TRADITIONAL_CHINESE; - Locale.setDefault(Locale.TRADITIONAL_CHINESE); + Locale.setDefault(locale); } @Test @@ -185,6 +188,6 @@ public class PrettyTimeI18n_zh_TW_Test { @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeLocaleFallbackTest.java b/src/test/java/org/xbib/time/pretty/PrettyTimeLocaleFallbackTest.java index 752bc45..eb581e6 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeLocaleFallbackTest.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeLocaleFallbackTest.java @@ -11,13 +11,11 @@ import static org.junit.Assert.assertEquals; public class PrettyTimeLocaleFallbackTest { - // Stores current locale so that it can be restored - private Locale locale; + private Locale defaultLocale; - // Method setUp() is called automatically before every test method @Before public void setUp() throws Exception { - locale = Locale.getDefault(); + defaultLocale = Locale.getDefault(); Locale.setDefault(new Locale("Foo", "Bar")); } @@ -32,7 +30,7 @@ public class PrettyTimeLocaleFallbackTest { @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeTest.java b/src/test/java/org/xbib/time/pretty/PrettyTimeTest.java index f30520c..65ca363 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeTest.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeTest.java @@ -15,11 +15,11 @@ import static org.junit.Assert.assertTrue; public class PrettyTimeTest { - private Locale locale; + private Locale defaultLocale; @Before public void setUp() throws Exception { - locale = Locale.getDefault(); + defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.ROOT); } @@ -236,7 +236,6 @@ public class PrettyTimeTest { @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } - } diff --git a/src/test/java/org/xbib/time/pretty/PrettyTimeUnitConfigurationTest.java b/src/test/java/org/xbib/time/pretty/PrettyTimeUnitConfigurationTest.java index 75f3379..d11884d 100644 --- a/src/test/java/org/xbib/time/pretty/PrettyTimeUnitConfigurationTest.java +++ b/src/test/java/org/xbib/time/pretty/PrettyTimeUnitConfigurationTest.java @@ -17,11 +17,11 @@ import static org.junit.Assert.assertEquals; public class PrettyTimeUnitConfigurationTest { - private Locale locale; + private Locale defaultLocale; @Before public void setUp() throws Exception { - locale = Locale.getDefault(); + defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.ROOT); } @@ -53,7 +53,6 @@ public class PrettyTimeUnitConfigurationTest { @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } - } diff --git a/src/test/java/org/xbib/time/pretty/SimpleTimeFormatTest.java b/src/test/java/org/xbib/time/pretty/SimpleTimeFormatTest.java index 0d46260..4b1736a 100644 --- a/src/test/java/org/xbib/time/pretty/SimpleTimeFormatTest.java +++ b/src/test/java/org/xbib/time/pretty/SimpleTimeFormatTest.java @@ -13,11 +13,11 @@ import static org.junit.Assert.assertEquals; public class SimpleTimeFormatTest { - private Locale locale; + private Locale defaultLocale; @Before public void setUp() throws Exception { - locale = Locale.getDefault(); + defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.ROOT); } @@ -46,7 +46,7 @@ public class SimpleTimeFormatTest { @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/pretty/i18n/SimpleTimeFormatTimeQuantifiedNameTest.java b/src/test/java/org/xbib/time/pretty/i18n/SimpleTimeFormatTimeQuantifiedNameTest.java index 8ec02cc..f5f2467 100644 --- a/src/test/java/org/xbib/time/pretty/i18n/SimpleTimeFormatTimeQuantifiedNameTest.java +++ b/src/test/java/org/xbib/time/pretty/i18n/SimpleTimeFormatTimeQuantifiedNameTest.java @@ -12,12 +12,16 @@ import java.time.ZoneId; import java.util.Locale; public class SimpleTimeFormatTimeQuantifiedNameTest { + + private Locale defaultLocale; + private Locale locale; @Before public void setUp() throws Exception { - locale = Locale.getDefault(); - Locale.setDefault(new Locale("yy")); + defaultLocale = Locale.getDefault(); + locale = new Locale("yy"); + Locale.setDefault(locale); } @Test @@ -76,10 +80,8 @@ public class SimpleTimeFormatTimeQuantifiedNameTest { Assert.assertEquals("1 hour ago", p.format(0)); } - // Method tearDown() is called automatically after every test method @After public void tearDown() throws Exception { - Locale.setDefault(locale); + Locale.setDefault(defaultLocale); } - } diff --git a/src/test/java/org/xbib/time/pretty/i18n/TimeFormatProviderTest.java b/src/test/java/org/xbib/time/pretty/i18n/TimeFormatProviderTest.java index 34e5f7f..a4c8c76 100644 --- a/src/test/java/org/xbib/time/pretty/i18n/TimeFormatProviderTest.java +++ b/src/test/java/org/xbib/time/pretty/i18n/TimeFormatProviderTest.java @@ -9,21 +9,26 @@ import java.util.Locale; import java.util.ResourceBundle; public class TimeFormatProviderTest { + @Test public void test() { + Locale defaultLocale = Locale.getDefault(); Locale locale = new Locale("xx"); Locale.setDefault(locale); ResourceBundle bundle = ResourceBundle.getBundle(Resources.class.getName(), locale); Assert.assertTrue(bundle instanceof TimeFormatProvider); + Locale.setDefault(defaultLocale); } @Test public void testFormatFromDirectFormatOverride() throws Exception { + Locale defaultLocale = Locale.getDefault(); Locale locale = new Locale("xx"); Locale.setDefault(locale); PrettyTime prettyTime = new PrettyTime(locale); String result = prettyTime.format(System.currentTimeMillis() + 1000 * 60 * 6); Assert.assertEquals("6 minutes from now", result); + Locale.setDefault(defaultLocale); } } diff --git a/src/test/java/org/xbib/time/schedule/CompareBehaviorToQuartzTest.java b/src/test/java/org/xbib/time/schedule/CompareBehaviorToQuartzTest.java new file mode 100644 index 0000000..e295cbc --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/CompareBehaviorToQuartzTest.java @@ -0,0 +1,261 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import static org.xbib.time.schedule.DateTimes.toDates; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class CompareBehaviorToQuartzTest { + + static final CronExpression.Parser quartzLike = CronExpression.parser() + .withSecondsField(true) + .withOneBasedDayOfWeek(true) + .allowBothDayFields(false); + private static final String timeFormatString = "s m H d M E yyyy"; + private static final DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(timeFormatString); + private final DateFormat dateFormat = new SimpleDateFormat(timeFormatString); + + protected String string; + private Times expected; + + @Before + public void before() { + expected = new Times(); + } + + @Test + public void complex() throws Exception { + string = "0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"; + expected.seconds.with(0); + expected.minutes.with(52).withRange(3, 39); + expected.months.with(1, 3, 9); + expected.daysOfWeek.withRange(1, 5); + expected.years.withRange(2002, 2010); + check(); + } + + @Test + public void at_noon_every_day() throws Exception { + string = "0 0 12 * * ?"; + expected.seconds.with(0); + expected.minutes.with(0); + expected.hours.with(12); + check(); + } + + @Test + public void at_10_15am_every_day1() throws Exception { + string = "0 15 10 ? * *"; + expected.seconds.with(0); + expected.minutes.with(15); + expected.hours.with(10); + check(); + } + + @Test + public void at_10_15am_every_day2() throws Exception { + string = "0 15 10 * * ?"; + expected.seconds.with(0); + expected.minutes.with(15); + expected.hours.with(10); + check(); + } + + @Test + public void at_10_15am_every_day3() throws Exception { + string = "0 15 10 * * ? *"; + expected.seconds.with(0); + expected.minutes.with(15); + expected.hours.with(10); + check(); + } + + + @Test + public void at_10_15am_every_day_in_2005() throws Exception { + string = "0 15 10 * * ? 2005"; + expected.seconds.with(0); + expected.minutes.with(15); + expected.hours.with(10); + expected.years.with(2005); + check(); + } + + @Test + public void every_minute_of_2pm() throws Exception { + string = "0 * 14 * * ?"; + expected.seconds.with(0); + expected.hours.with(14); + check(); + } + + @Test + public void every_5_minutes_of_2pm() throws Exception { + string = "0 0/5 14 * * ?"; + expected.seconds.with(0); + expected.minutes.withRange(0, 59, 5); + expected.hours.with(14); + check(); + } + + @Test + public void every_5_minutes_of_2pm_and_6pm() throws Exception { + string = "0 0/5 14,18 * * ?"; + expected.seconds.with(0); + expected.minutes.withRange(0, 59, 5); + expected.hours.with(14, 18); + check(); + } + + @Test + public void first_5_minutes_of_2pm() throws Exception { + string = "0 0-5 14 * * ?"; + expected.seconds.with(0); + expected.minutes.withRange(0, 5); + expected.hours.with(14); + check(); + } + + @Test + public void at_2_10pm_and_2_44pm_every_wednesday_in_march() throws Exception { + string = "0 10,44 14 ? 3 WED"; + expected.seconds.with(0); + expected.minutes.with(10, 44); + expected.hours.with(14); + expected.months.with(3); + expected.daysOfWeek.with(3); + check(); + } + + @Test + public void at_10_15am_every_weekday() throws Exception { + string = "0 15 10 ? * MON-FRI"; + expected.seconds.with(0); + expected.minutes.with(15); + expected.hours.with(10); + expected.daysOfWeek.withRange(1, 5); + check(); + } + + @Test + public void at_10_15am_on_the_15th_of_every_month() throws Exception { + string = "0 15 10 15 * ?"; + expected.seconds.with(0); + expected.minutes.with(15); + expected.hours.with(10); + expected.daysOfMonth.with(15); + check(); + } + + @Test + public void at_10_15am_on_the_last_day_of_every_month() throws Exception { + string = "0 15 10 L * ?"; + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().withDayOfYear(1).truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(t.with(TemporalAdjusters.lastDayOfMonth())); + t = t.plusMonths(1); + } + check(times); + } + + @Test + public void at_10_15am_on_the_last_friday_of_every_month() throws Exception { + string = "0 15 10 ? * 6L"; + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(DateTimes.lastOfMonth(t, DayOfWeek.FRIDAY)); + t = t.plusMonths(1); + } + check(times); + } + + @Test + public void at_10_15am_on_the_last_friday_of_every_month_during_2002_through_2005() throws Exception { + string = "0 15 10 ? * 6L 2002-2005"; + List times = new ArrayList<>(); + for (int year = 2002; year <= 2005; year++) { + ZonedDateTime t = ZonedDateTime.now().withYear(year).truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15); + while (t.getYear() == year) { + times.add(DateTimes.lastOfMonth(t, DayOfWeek.FRIDAY)); + t = t.plusMonths(1); + } + } + check(times); + } + + + @Test + @Ignore + // TODO let's see if we can make this more reliably faster than the respective quartz run + public void at_10_15am_on_the_third_friday_of_every_month() throws Exception { + string = "0 15 10 ? * 6#3"; + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().withDayOfYear(1).truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(DateTimes.nthOfMonth(t, DayOfWeek.FRIDAY, 3)); + t = t.plusMonths(1); + } + check(times); + } + + @Test + public void at_noon_every_5_days_every_month_starting_on_the_first_day_of_the_month() throws Exception { + string = "0 0 12 1/5 * ?"; + expected.seconds.with(0); + expected.minutes.with(0); + expected.hours.with(12); + expected.daysOfMonth.withRange(1, 31, 5); + check(); + } + + @Test + public void november_11th_at_11_11am() throws Exception { + string = "0 11 11 11 11 ?"; + expected.seconds.with(0); + expected.minutes.with(11); + expected.hours.with(11); + expected.daysOfMonth.with(11); + expected.months.with(11); + check(); + } + + private void check() throws ParseException { + check(expected.dateTimes()); + } + + protected void check(Iterable times) throws ParseException { + checkLocalImplementation(times); + checkQuartzImplementation(toDates(times)); + } + + private void checkQuartzImplementation(Iterable times) throws ParseException { + org.quartz.CronExpression quartz = new org.quartz.CronExpression(string); + for (Date time : times) { + assertTrue(dateFormat.format(time).toUpperCase() + " doesn't match expression: " + string, quartz.isSatisfiedBy(time)); + } + } + + private void checkLocalImplementation(Iterable times) { + CronExpression expr = quartzLike.parse(string); + for (ZonedDateTime time : times) { + assertTrue(time.format(dateTimeFormat).toUpperCase() + " doesn't match expression: " + string, expr.matches(time)); + } + } +} diff --git a/src/test/java/org/xbib/time/schedule/CompareSizeToQuartzTest.java b/src/test/java/org/xbib/time/schedule/CompareSizeToQuartzTest.java new file mode 100644 index 0000000..9e63ab3 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/CompareSizeToQuartzTest.java @@ -0,0 +1,123 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import com.google.caliper.memory.ObjectGraphMeasurer; +import org.junit.Test; + +public class CompareSizeToQuartzTest { + + private static final CronExpression.Parser quartzLike = CronExpression.parser() + .withSecondsField(true) + .withOneBasedDayOfWeek(true) + .allowBothDayFields(false); + + @Test + public void complex() throws Exception { + check("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"); + } + + @Test + public void at_noon_every_day() throws Exception { + check("0 0 12 * * ?"); + } + + @Test + public void at_10_15am_every_day1() throws Exception { + check("0 15 10 ? * *"); + } + + @Test + public void at_10_15am_every_day2() throws Exception { + check("0 15 10 * * ?"); + } + + @Test + public void at_10_15am_every_day3() throws Exception { + check("0 15 10 * * ? *"); + } + + @Test + public void at_10_15am_every_day_in_2005() throws Exception { + check("0 15 10 * * ? 2005"); + } + + @Test + public void every_minute_of_2pm() throws Exception { + check("0 * 14 * * ?"); + } + + @Test + public void every_5_minutes_of_2pm() throws Exception { + check("0 0/5 14 * * ?"); + } + + @Test + public void every_5_minutes_of_2pm_and_6pm() throws Exception { + check("0 0/5 14,18 * * ?"); + } + + @Test + public void first_5_minutes_of_2pm() throws Exception { + check("0 0-5 14 * * ?"); + } + + @Test + public void at_2_10pm_and_2_44pm_every_wednesday_in_march() throws Exception { + check("0 10,44 14 ? 3 WED"); + } + + @Test + public void at_10_15am_every_weekday() throws Exception { + check("0 15 10 ? * MON-FRI"); + } + + @Test + public void at_10_15am_on_the_15th_of_every_month() throws Exception { + check("0 15 10 15 * ?"); + } + + @Test + public void at_10_15am_on_the_last_day_of_every_month() throws Exception { + check("0 15 10 L * ?"); + } + + @Test + public void at_10_15am_on_the_last_friday_of_every_month() throws Exception { + check("0 15 10 ? * 6L"); + } + + @Test + public void at_10_15am_on_the_last_friday_of_every_month_during_2002_through_2005() throws Exception { + check("0 15 10 ? * 6L 2002-2005"); + } + + @Test + public void at_10_15am_on_the_third_friday_of_every_month() throws Exception { + check("0 15 10 ? * 6#3"); + } + + @Test + public void at_noon_every_5_days_every_month_starting_on_the_first_day_of_the_month() throws Exception { + check("0 0 12 1/5 * ?"); + } + + @Test + public void november_11th_at_11_11am() throws Exception { + check("0 11 11 11 11 ?"); + } + + private void check(String expression) throws Exception { + CronExpression local = quartzLike.parse(expression); + org.quartz.CronExpression quartz = new org.quartz.CronExpression(expression); + long localSize = ObjectSizeCalculator.getObjectSize(local); + long quartzSize = ObjectSizeCalculator.getObjectSize(quartz); + assertTrue("We have more bytes", localSize < quartzSize); + ObjectGraphMeasurer.Footprint localFoot = ObjectGraphMeasurer.measure(local); + ObjectGraphMeasurer.Footprint quartzFoot = ObjectGraphMeasurer.measure(quartz); + assertTrue("We have more references", localFoot.getAllReferences() < quartzFoot.getAllReferences()); + assertTrue("We have more non-null references", localFoot.getNonNullReferences() < quartzFoot.getNonNullReferences()); + //assertTrue("We have more null references", localFoot.getNullReferences() < quartzFoot.getNullReferences()); + assertTrue("We have more objects", localFoot.getObjects() < quartzFoot.getObjects()); + assertTrue("We have more primitives", localFoot.getPrimitives().size() < quartzFoot.getPrimitives().size()); + } +} diff --git a/src/test/java/org/xbib/time/schedule/CompareSpeedToQuartzTest.java b/src/test/java/org/xbib/time/schedule/CompareSpeedToQuartzTest.java new file mode 100644 index 0000000..abc5a94 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/CompareSpeedToQuartzTest.java @@ -0,0 +1,66 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import com.google.common.base.Stopwatch; +import java.text.ParseException; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +public class CompareSpeedToQuartzTest extends CompareBehaviorToQuartzTest { + @Override + protected void check(final Iterable times) throws ParseException { + final Iterable dates = DateTimes.toDates(times); + final CronExpression local = quartzLike.parse(string); + final org.quartz.CronExpression quartz = new org.quartz.CronExpression(string); + final int trials = 25; + final Stopwatch clock = Stopwatch.createStarted(); + for (int i = 0; i < trials; i++) { + for (ZonedDateTime time : times) { + local.matches(time); + } + } + final long localNano = clock.elapsed(TimeUnit.NANOSECONDS); + clock.reset().start(); + for (int i = 0; i < trials; i++) { + for (Date date : dates) { + quartz.isSatisfiedBy(date); + } + } + final long quartzNano = clock.elapsed(TimeUnit.NANOSECONDS); + final boolean lessThanOrEqual = localNano <= quartzNano; + System.out.printf( + "%-80s %-60s local %8.2fms %6s Quartz %8.2fms\n", + nameOfTestMethod(), + string, + localNano / 1000000d, + (lessThanOrEqual ? "<=" : ">"), + quartzNano / 1000000d + ); + assertTrue( + "We took longer for expression '" + string + "'; " + localNano + " > " + quartzNano, + lessThanOrEqual + ); + } + + private String nameOfTestMethod() { + try { + throw new Exception(); + } catch (Exception e) { + String method = null; + Iterator trace = Arrays.asList(e.getStackTrace()).iterator(); + StackTraceElement element = trace.next(); + while (getClass().getName().equals(element.getClassName())) { + element = trace.next(); + } + String parentClassName = getClass().getSuperclass().getName(); + while (element.getClassName().equals(parentClassName)) { + method = element.getMethodName(); + element = trace.next(); + } + return method; + } + } +} diff --git a/src/test/java/org/xbib/time/schedule/CronExpressionTest.java b/src/test/java/org/xbib/time/schedule/CronExpressionTest.java new file mode 100644 index 0000000..a05c1e2 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/CronExpressionTest.java @@ -0,0 +1,344 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.xbib.time.schedule.DateTimes.midnight; +import static org.xbib.time.schedule.DateTimes.nearestWeekday; +import static org.xbib.time.schedule.DateTimes.now; +import static org.xbib.time.schedule.DateTimes.nthOfMonth; +import static org.xbib.time.schedule.DateTimes.startOfHour; +import org.junit.Test; +import java.time.DayOfWeek; +import java.time.Month; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +public class CronExpressionTest { + + private static final CronExpression.Parser withSecondsField = CronExpression.parser().withSecondsField(true); + + private CronExpression expression; + + @Test + public void testHashCode() { + assertEquals(CronExpression.daily().hashCode(), CronExpression.parse("@daily").hashCode()); + assertEquals(CronExpression.daily().hashCode(), CronExpression.parse("@midnight").hashCode()); + assertEquals(CronExpression.hourly().hashCode(), CronExpression.parse("@hourly").hashCode()); + assertEquals(CronExpression.monthly().hashCode(), CronExpression.parse("@monthly").hashCode()); + assertEquals(CronExpression.weekly().hashCode(), CronExpression.parse("@weekly").hashCode()); + assertEquals(CronExpression.yearly().hashCode(), CronExpression.parse("@annually").hashCode()); + assertEquals(CronExpression.yearly().hashCode(), CronExpression.parse("@yearly").hashCode()); + assertEquals( + CronExpression.parse("0 0 ? * 5#3,2#2").hashCode(), + CronExpression.parse("0 0 ? * 5#3,2#2").hashCode()); + assertEquals( + withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010").hashCode(), + withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010").hashCode()); + } + + @Test + public void testEquals() { + assertEquals(CronExpression.daily(), CronExpression.parse("@daily")); + assertEquals(CronExpression.daily(), CronExpression.parse("@midnight")); + assertEquals(CronExpression.hourly(), CronExpression.parse("@hourly")); + assertEquals(CronExpression.monthly(), CronExpression.parse("@monthly")); + assertEquals(CronExpression.weekly(), CronExpression.parse("@weekly")); + assertEquals(CronExpression.yearly(), CronExpression.parse("@annually")); + assertEquals(CronExpression.yearly(), CronExpression.parse("@yearly")); + assertEquals( + CronExpression.parse("0 0 ? * 5#3,2#2"), + CronExpression.parse("0 0 ? * 5#3,2#2")); + assertEquals( + withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"), + withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010")); + } + + @Test + public void illegalCharacter() { + try { + expression = CronExpression.parse("0 0 4X * *"); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Bad character 'X' at position 5 in string: 0 0 4X * *", e.getMessage()); + } + } + + @Test + public void disallowBothDayFields() { + try { + expression = CronExpression.parser().allowBothDayFields(false).parse("0 0 1 * 5L"); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Day of month and day of week may not both be specified", e.getMessage()); + } + } + + @Test + public void nearestWeekdayWithoutNumber() { + try { + expression = CronExpression.parse("0 0 W * *"); + } catch (IllegalArgumentException e) { + assertEquals("Bad character 'W' in day of month field: W", e.getMessage()); + } + } + + @Test + public void nearestWeekdayOfMonth() { + expression = CronExpression.parse("0 0 5W * *"); + List times = new ArrayList<>(); + ZonedDateTime t = DateTimes.startOfYear(); + int year = t.getYear(); + do { + times.add(nearestWeekday(t.withDayOfMonth(5))); + t = t.plusMonths(1); + } while (year == t.getYear()); + assertMatchesAll(times); + } + + @Test + public void nearestFriday() { + ZonedDateTime t = now().truncatedTo(ChronoUnit.DAYS).with(DayOfWeek.SATURDAY); + expression = CronExpression.parse("0 0 " + t.getDayOfMonth() + "W * *"); + assertMatches(t.minusDays(1)); + } + + @Test + public void nearestMonday() { + ZonedDateTime t = now().truncatedTo(ChronoUnit.DAYS).with(DayOfWeek.SUNDAY); + expression = CronExpression.parse("0 0 " + t.getDayOfMonth() + "W * *"); + assertMatches(t.plusDays(1)); + } + + @Test + public void nonMatchingNth() { + expression = CronExpression.parse("0 0 ? * 2#2"); + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(nthOfMonth(t, DayOfWeek.TUESDAY, 1)); + t = t.plusMonths(1); + } + for (ZonedDateTime time : times) { + assertFalse(expression.matches(time)); + } + } + + @Test + public void multipleNth() { + expression = CronExpression.parse("0 0 ? * 5#3,2#2"); + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(nthOfMonth(t, DayOfWeek.FRIDAY, 3)); + times.add(nthOfMonth(t, DayOfWeek.TUESDAY, 2)); + t = t.plusMonths(1); + } + assertMatchesAll(times); + } + + @Test + public void thirdFriday() { + String string = "0 0 ? * 5#3"; + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(nthOfMonth(t, DayOfWeek.FRIDAY, 3)); + t = t.plusMonths(1); + } + expression = CronExpression.parse(string); + assertMatchesAll(times); + } + + @Test + public void reboot() { + expression = CronExpression.parse("@reboot"); + ZonedDateTime now = now(); + assertTrue(expression.matches(now)); + assertFalse(expression.matches(now)); + } + + @Test + public void minuteFullRangeExplicit() { + expression = CronExpression.parse("0-59 * * * * *"); + ZonedDateTime time = startOfHour(); + int hour = time.getHour(); + do { + assertMatches(time); + time = time.plusMinutes(1); + } while (time.getHour() == hour); + } + + @Test + public void minuteRestrictedRange() { + expression = CronExpression.parse("10-20 * * * * *"); + int first = 10, last = 20; + ZonedDateTime time = startOfHour(); + int hour = time.getHour(); + do { + int minute = time.getMinute(); + assertEquals(first <= minute && minute <= last, expression.matches(time)); + time = time.plusMinutes(1); + } while (time.getHour() == hour); + } + + @Test + public void minuteFullRangeMod() { + expression = CronExpression.parse("*/5 * * * * *"); + ZonedDateTime time = startOfHour(); + int hour = time.getHour(); + do { + int minute = time.getMinute(); + assertEquals(minute % 5 == 0, expression.matches(time)); + time = time.plusMinutes(1); + } while (time.getHour() == hour); + } + + @Test + public void minuteRestrictedRangeMod() { + expression = CronExpression.parse("10-20/5 * * * * *"); + int first = 10, last = 20; + ZonedDateTime time = startOfHour(); + int hour = time.getHour(); + do { + int minute = time.getMinute(); + assertEquals(first <= minute && minute <= last && minute % 5 == 0, expression.matches(time)); + time = time.plusMinutes(1); + } while (time.getHour() == hour); + } + + @Test + public void yearly() { + expression = CronExpression.yearly(); + assertYearly(); + } + + @Test + public void monthly() { + expression = CronExpression.monthly(); + assertMonthly(); + } + + @Test + public void weekly() { + expression = CronExpression.weekly(); + assertWeekly(); + } + + @Test + public void daily() { + expression = CronExpression.daily(); + assertDaily(); + } + + @Test + public void hourly() { + expression = CronExpression.hourly(); + assertHourly(); + } + + @Test + public void yearlyKeyword() { + expression = CronExpression.parse("@yearly"); + assertYearly(); + } + + @Test + public void annualKeyword() { + expression = CronExpression.parse("@annually"); + assertYearly(); + } + + @Test + public void monthlyKeyword() { + expression = CronExpression.parse("@monthly"); + assertMonthly(); + } + + @Test + public void weeklyKeyword() { + expression = CronExpression.parse("@weekly"); + assertWeekly(); + } + + @Test + public void dailyKeyword() { + expression = CronExpression.parse("@daily"); + assertDaily(); + } + + @Test + public void hourlyKeyword() { + expression = CronExpression.parse("@hourly"); + assertHourly(); + } + + @Test + public void invalid() { + assertFalse(CronExpression.isValid(null)); + assertFalse(CronExpression.isValid("")); + assertFalse(CronExpression.isValid("a")); + assertFalse(CronExpression.isValid("0 0 1 * X")); + assertFalse(CronExpression.isValid("0 0 1 * 1X")); + } + + @Test + public void invalidDueToSecondsField() { + assertTrue(CronExpression.isValid("0 0 1 * 1")); + assertFalse(CronExpression.parser().allowBothDayFields(false).isValid("0 0 1 * 1")); + } + + private void assertWeekly() { + for (int week = 1; week <= 52; week++) { + assertMatches(midnight().withDayOfYear(7 * week).with(DayOfWeek.SUNDAY)); + } + } + + private void assertDaily() { + for (int day = 1; day <= 365; day++) { + assertMatches(midnight().withDayOfYear(day)); + } + } + + private void assertMonthly() { + for (Month month : Month.values()) { + assertMatches(midnight().with(month).withDayOfMonth(1)); + } + } + + private void assertHourly() { + for (int day = 1; day <= 365; day++) { + for (int hour = 0; hour <= 23; hour++) { + assertMatches(midnight().withDayOfYear(day).withHour(hour)); + } + } + } + + private void assertYearly() { + assertMatches(midnight().withDayOfYear(1)); + } + + private static final String formatString = "m H d M E yyyy"; + + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + + private void assertMatchesAll(List times) { + for (ZonedDateTime time : times) { + assertMatches(time); + } + } + + private void assertMatches(ZonedDateTime time) { + assertTrue( + time.format(formatter).toUpperCase() + " doesn't match expression: " + expression, + expression.matches(time) + ); + } +} diff --git a/src/test/java/org/xbib/time/schedule/CronScheduleTest.java b/src/test/java/org/xbib/time/schedule/CronScheduleTest.java new file mode 100644 index 0000000..82ecfcc --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/CronScheduleTest.java @@ -0,0 +1,126 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CronScheduleTest { + + private static final Logger logger = Logger.getLogger(CronScheduleTest.class.getName()); + + private CronSchedule schedule; + + private ScheduledExecutorService executor; + + @Before + public void before() { + executor = Executors.newScheduledThreadPool(1); + } + + @Test + public void runMinutes() throws InterruptedException { + schedule = new CronSchedule<>(executor, 60000); + final AtomicBoolean run = new AtomicBoolean(false); + schedule.add("test", CronExpression.parser() + .parse("* * * * * *"), + () -> { + run.set(true); + return null; + }); + assertFalse(run.get()); + schedule.start(); + Thread.sleep(TimeUnit.MINUTES.toMillis(2)); + assertTrue(run.get()); + logger.log(Level.INFO, schedule.toString()); + } + + @Test + public void runSeconds() throws Exception { + schedule = new CronSchedule<>(executor, 1000); + final AtomicBoolean run = new AtomicBoolean(false); + schedule.add("test", CronExpression.parser() + .withSecondsField(true).parse("* * * * * *"), + () -> { + run.set(true); + return null; + }); + assertFalse(run.get()); + schedule.start(); + Thread.sleep(TimeUnit.SECONDS.toMillis(2)); + assertTrue(run.get()); + } + + @Test + public void removeOne() throws Exception { + schedule = new CronSchedule<>(executor, 1000); + final Multiset counts = HashMultiset.create(); + Callable a = () -> { + counts.add("a"); + return null; + }; + Callable b = () -> { + counts.add("b"); + return null; + }; + CronExpression expression = CronExpression.parse("* * * * *"); + schedule.add("1", expression, a); + schedule.add("2", expression, b); + runAndWait(); + assertEquals(1, counts.count("a")); + assertEquals(1, counts.count("b")); + schedule.remove("1"); + runAndWait(); + assertEquals(1, counts.count("a")); + assertEquals(2, counts.count("b")); + } + + @Test + public void removeAllForExpression() throws Exception { + schedule = new CronSchedule<>(executor, 1000); + final Multiset counts = HashMultiset.create(); + Callable a = () -> { + counts.add("a"); + return null; + }; + Callable b = () -> { + counts.add("b"); + return null; + }; + CronExpression expression = CronExpression.parse("* * * * *"); + schedule.add("a", expression, a); + schedule.add("b", expression, b); + runAndWait(); + assertEquals(1, counts.count("a")); + assertEquals(1, counts.count("b")); + schedule.remove("a"); + schedule.remove("b"); + runAndWait(); + assertEquals(1, counts.count("a")); + assertEquals(1, counts.count("b")); + } + + @After + public void after() throws IOException { + if (schedule != null) { + schedule.close(); + } + } + + private void runAndWait() throws InterruptedException { + schedule.run(); + Thread.sleep(10); + } +} diff --git a/src/test/java/org/xbib/time/schedule/DateTimes.java b/src/test/java/org/xbib/time/schedule/DateTimes.java new file mode 100644 index 0000000..3c52c05 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/DateTimes.java @@ -0,0 +1,65 @@ +package org.xbib.time.schedule; + +import java.time.DayOfWeek; +import java.time.Month; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.Date; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class DateTimes { + + static Iterable toDates(Iterable times) { + return StreamSupport.stream(times.spliterator(), false) + .map(input -> Date.from(input.toInstant())).collect(Collectors.toList()); + } + + static ZonedDateTime midnight() { + return now().truncatedTo(ChronoUnit.DAYS); + } + + static ZonedDateTime startOfHour() { + return now().truncatedTo(ChronoUnit.HOURS); + } + + public static ZonedDateTime now() { + return ZonedDateTime.now(); + } + + static ZonedDateTime lastOfMonth(ZonedDateTime t, DayOfWeek dayOfWeek) { + ZonedDateTime day = t.with(TemporalAdjusters.lastDayOfMonth()).with(dayOfWeek); + if (day.getMonth() != t.getMonth()) { + day = day.minusWeeks(1); + } + return day; + } + + static ZonedDateTime nthOfMonth(ZonedDateTime t, DayOfWeek dayOfWeek, int desiredNumber) { + Month month = t.getMonth(); + t = t.withDayOfMonth(1).with(dayOfWeek); + if (t.getMonth() != month) { + t = t.plusWeeks(1); + } + int number = 1; + while (number < desiredNumber && t.getMonth() == month) { + number++; + t = t.plusWeeks(1); + } + return t; + } + + static ZonedDateTime nearestWeekday(ZonedDateTime t) { + if (t.getDayOfWeek() == DayOfWeek.SATURDAY) { + return t.minusDays(1); + } else if (t.getDayOfWeek() == DayOfWeek.SUNDAY) { + return t.plusDays(1); + } + return t; + } + + static ZonedDateTime startOfYear() { + return midnight().withDayOfYear(1); + } +} diff --git a/src/test/java/org/xbib/time/schedule/DayOfMonthFieldTest.java b/src/test/java/org/xbib/time/schedule/DayOfMonthFieldTest.java new file mode 100644 index 0000000..dc57632 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/DayOfMonthFieldTest.java @@ -0,0 +1,36 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAdjusters; + +public class DayOfMonthFieldTest { + + @Test + public void last() { + DayOfMonthField field = parse("L"); + assertTrue(field.matches(ZonedDateTime.now().with(TemporalAdjusters.lastDayOfMonth()))); + } + + @Test + public void nearestFriday() { + ZonedDateTime saturday = ZonedDateTime.now().with(DayOfWeek.SATURDAY); + ZonedDateTime friday = saturday.minusDays(1); + DayOfMonthField field = parse(saturday.getDayOfMonth() + "W"); + assertTrue(field.matches(friday)); + } + + @Test + public void nearestMonday() { + ZonedDateTime sunday = ZonedDateTime.now().with(DayOfWeek.SUNDAY); + ZonedDateTime monday = sunday.plusDays(1); + DayOfMonthField field = parse(sunday.getDayOfMonth() + "W"); + assertTrue(field.matches(monday)); + } + + private DayOfMonthField parse(String s) { + return DayOfMonthField.parse(new Tokens(s)); + } +} diff --git a/src/test/java/org/xbib/time/schedule/DayOfWeekFieldTest.java b/src/test/java/org/xbib/time/schedule/DayOfWeekFieldTest.java new file mode 100644 index 0000000..f862844 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/DayOfWeekFieldTest.java @@ -0,0 +1,28 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class DayOfWeekFieldTest { + + @Test + public void keywords() { + assertTrue(parse("MON", false).contains(1)); + assertTrue(parse("TUE", false).contains(2)); + assertTrue(parse("WED", false).contains(3)); + assertTrue(parse("THU", false).contains(4)); + assertTrue(parse("FRI", false).contains(5)); + assertTrue(parse("SAT", false).contains(6)); + assertTrue(parse("SUN", false).contains(7)); + } + + @Test + public void oneBased() { + assertTrue(parse("1", true).contains(0)); + assertTrue(parse("2", true).contains(1)); + } + + private DayOfWeekField parse(String s, boolean oneBased) { + return DayOfWeekField.parse(new Tokens(s), oneBased); + } +} diff --git a/src/test/java/org/xbib/time/schedule/DefaultFieldTest.java b/src/test/java/org/xbib/time/schedule/DefaultFieldTest.java new file mode 100644 index 0000000..fdbb3a9 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/DefaultFieldTest.java @@ -0,0 +1,125 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Test; + +public class DefaultFieldTest { + + private DefaultField field; + + @Test + public void emptyMonthField() { + parse("", 1, 12); + for (int month = 1; month <= 12; month++) { + assertFalse(field.contains(month)); + } + } + + @Test + public void backwardsRange() { + parse("2-1", 1, 2); + assertFalse(field.contains(0)); + assertFalse(field.contains(1)); + assertFalse(field.contains(2)); + assertFalse(field.contains(3)); + } + + @Test + public void finalWildcardWins() { + parse("1-2,2,3,*", 1, 10); + assertContainsRange(1, 10); + } + + @Test + public void initialWildcardWins() { + parse("*,1-2,2,3", 1, 10); + assertContainsRange(1, 10); + } + + @Test + public void multipleRanges() { + parse("1-2,3-4", 1, 5); + assertContains(1, 2, 3, 4); + } + + @Test + public void multipleNumbers() { + parse("1,2,3", 1, 5); + assertContains(1, 2, 3); + } + + @Test + public void danglingRange() { + try { + parse("1-", 0, 0); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertEquals("Expected number", e.getMessage()); + } + } + + @Test + public void danglingSkip() { + try { + parse("1-2/", 0, 0); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertEquals("Expected number", e.getMessage()); + } + } + + @Test + public void range() { + parse("1-12", 1, 12); + assertContainsRange(1, 12); + } + + @Test + public void wildcard() { + parse("*", 1, 12); + assertContainsRange(1, 12); + } + + @Test + public void skipRangeWithImplicitEnd() { + parse("1/5", 1, 31); + assertContains(1, 6, 11, 16, 21, 26, 31); + } + + @Test + public void oneBasedSkipRange() { + parse("1-31/5", 1, 31); + assertContains(1, 6, 11, 16, 21, 26, 31); + } + + @Test + public void zeroBasedSkipRange() { + parse("0-20/5", 0, 59); + assertContains(0, 5, 10, 15, 20); + } + + @Test + public void wildcardSkipRange() { + parse("*/5", 0, 20); + assertContains(0, 5, 10, 15, 20); + } + + private void assertContains(int... numbers) { + for (int number : numbers) { + assertTrue(field.contains(number)); + } + } + + private void assertContainsRange(int first, int last) { + for (int number = first; number <= last; number++) { + assertContains(number); + } + } + + private DefaultField parse(String s, int min, int max) { + return field = DefaultField.parse(new Tokens(s), min, max); + } +} diff --git a/src/test/java/org/xbib/time/schedule/Integers.java b/src/test/java/org/xbib/time/schedule/Integers.java new file mode 100644 index 0000000..677e43e --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/Integers.java @@ -0,0 +1,42 @@ +package org.xbib.time.schedule; + +import com.google.common.collect.ForwardingSet; +import java.util.HashSet; +import java.util.Set; + +public class Integers extends ForwardingSet { + private final Set delegate; + + public Integers(int... integers) { + delegate = new HashSet<>(); + with(integers); + } + + @Override + protected Set delegate() { + return delegate; + } + + public Integers with(int... integers) { + for (int integer : integers) { + add(integer); + } + return this; + } + + public Integers withRange(int start, int end) { + for (int i = start; i <= end; i++) { + add(i); + } + return this; + } + + public Integers withRange(int start, int end, int mod) { + for (int i = start; i <= end; i++) { + if ((i - start) % mod == 0) { + add(i); + } + } + return this; + } +} diff --git a/src/test/java/org/xbib/time/schedule/KeywordsTest.java b/src/test/java/org/xbib/time/schedule/KeywordsTest.java new file mode 100644 index 0000000..8767ca2 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/KeywordsTest.java @@ -0,0 +1,79 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import org.junit.Before; +import org.junit.Test; + +public class KeywordsTest { + private Keywords keywords; + + @Before + public void before() { + keywords = new Keywords(); + } + + @Test + public void normalUse() { + keywords.put("AAA", 1); + keywords.put("BBB", 2); + assertEquals(1, keywords.get("AAABBB", 0, 3)); + assertEquals(2, keywords.get("AAABBB", 3, 6)); + } + + @Test + public void getNotPresent() { + try { + assertEquals(-1, keywords.get("CCC", 0, 3)); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void getWrongAlphabet() { + try { + keywords.get("aaa", 0, 3); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void getWrongLength() { + try { + keywords.get("aaa", 0, 1); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void putEmpty() { + try { + keywords.put("", 0); + fail("Expected exception"); + } catch (StringIndexOutOfBoundsException e) { + assertEquals("String index out of range: 0", e.getMessage()); + } + } + + @Test + public void putWrongLength() { + try { + keywords.put("A", 0); + fail("Expected exception"); + } catch (StringIndexOutOfBoundsException e) { + assertEquals("String index out of range: 1", e.getMessage()); + } + } + + @Test + public void putWrongAlphabet() { + try { + keywords.put("a", 0); + fail("Expected exception"); + } catch (ArrayIndexOutOfBoundsException e) { + assertEquals("Index 32 out of bounds for length 26", e.getMessage()); + } + } +} diff --git a/src/test/java/org/xbib/time/schedule/MonthFieldTest.java b/src/test/java/org/xbib/time/schedule/MonthFieldTest.java new file mode 100644 index 0000000..83d161b --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/MonthFieldTest.java @@ -0,0 +1,27 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class MonthFieldTest { + + @Test + public void keywords() { + assertTrue(parse("JAN").contains(1)); + assertTrue(parse("FEB").contains(2)); + assertTrue(parse("MAR").contains(3)); + assertTrue(parse("APR").contains(4)); + assertTrue(parse("MAY").contains(5)); + assertTrue(parse("JUN").contains(6)); + assertTrue(parse("JUL").contains(7)); + assertTrue(parse("AUG").contains(8)); + assertTrue(parse("SEP").contains(9)); + assertTrue(parse("OCT").contains(10)); + assertTrue(parse("NOV").contains(11)); + assertTrue(parse("DEC").contains(12)); + } + + private DefaultField parse(String s) { + return MonthField.parse(new Tokens(s)); + } +} diff --git a/src/test/java/org/xbib/time/schedule/NextExecutionTest.java b/src/test/java/org/xbib/time/schedule/NextExecutionTest.java new file mode 100644 index 0000000..e931a13 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/NextExecutionTest.java @@ -0,0 +1,79 @@ +package org.xbib.time.schedule; + +import org.junit.Test; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NextExecutionTest { + + private static final Logger logger = Logger.getLogger(NextExecutionTest.class.getName()); + + @Test + public void nextSecond() { + CronExpression expression = CronExpression.parser().withSecondsField(true) + .parse("0-59 * * * * *"); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime next = expression.nextExecution(now, now.plusMinutes(1)); + logger.log(Level.INFO, now.toString()); + logger.log(Level.INFO, next.toString()); + } + + @Test + public void nextHour() { + CronExpression expression = CronExpression.hourly(); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime next = expression.nextExecution(now, now.plusHours(2)); + logger.log(Level.INFO, now.toString()); + logger.log(Level.INFO, next.toString()); + } + + @Test + public void nextDay() { + CronExpression expression = CronExpression.daily(); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime next = expression.nextExecution(now, now.plusDays(2)); + logger.log(Level.INFO, now.toString()); + logger.log(Level.INFO, next.toString()); + } + + @Test + public void nextWeek() { + CronExpression expression = CronExpression.weekly(); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime next = expression.nextExecution(now, now.plusWeeks(2)); + logger.log(Level.INFO, now.toString()); + logger.log(Level.INFO, next.toString()); + } + + @Test + public void nextMonth() { + CronExpression expression = CronExpression.monthly(); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime next = expression.nextExecution(now, now.plusMonths(2)); + logger.log(Level.INFO, now.toString()); + logger.log(Level.INFO, next.toString()); + } + + @Test + public void nextFractionMinute() { + CronExpression expression = CronExpression.parse("10-20/5 * * * * *"); + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime next = expression.nextExecution(now, now.plusHours(2)); + logger.log(Level.INFO, now.toString()); + logger.log(Level.INFO, next.toString()); + } + + @Test + public void nextMultipleNth() { + CronExpression expression = CronExpression.parser() + .withSecondsField(true) + .parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"); + ZonedDateTime begin = ZonedDateTime.of(2002, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()); + logger.log(Level.INFO, expression.toString()); + logger.log(Level.INFO, begin.toString()); + ZonedDateTime next = expression.nextExecution(begin, begin.plusYears(1)); + logger.log(Level.INFO, next.toString()); + } +} diff --git a/src/test/java/org/xbib/time/schedule/ObjectSizeCalculator.java b/src/test/java/org/xbib/time/schedule/ObjectSizeCalculator.java new file mode 100644 index 0000000..4435833 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/ObjectSizeCalculator.java @@ -0,0 +1,436 @@ +package org.xbib.time.schedule; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Sets; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * Contains utility methods for calculating the memory usage of objects. It + * only works on the HotSpot JVM, and infers the actual memory layout (32 bit + * vs. 64 bit word size, compressed object pointers vs. uncompressed) from + * best available indicators. It can reliably detect a 32 bit vs. 64 bit JVM. + * It can only make an educated guess at whether compressed OOPs are used, + * though; specifically, it knows what the JVM's default choice of OOP + * compression would be based on HotSpot version and maximum heap sizes, but if + * the choice is explicitly overridden with the -XX:{+|-}UseCompressedOops command line + * switch, it can not detect + * this fact and will report incorrect sizes, as it will presume the default JVM + * behavior. + */ +public class ObjectSizeCalculator { + + /** + * Describes constant memory overheads for various constructs in a JVM implementation. + */ + public interface MemoryLayoutSpecification { + + /** + * Returns the fixed overhead of an array of any type or length in this JVM. + * + * @return the fixed overhead of an array. + */ + int getArrayHeaderSize(); + + /** + * Returns the fixed overhead of for any {@link Object} subclass in this JVM. + * + * @return the fixed overhead of any object. + */ + int getObjectHeaderSize(); + + /** + * Returns the quantum field size for a field owned by an object in this JVM. + * + * @return the quantum field size for an object. + */ + int getObjectPadding(); + + /** + * Returns the fixed size of an object reference in this JVM. + * + * @return the size of all object references. + */ + int getReferenceSize(); + + /** + * Returns the quantum field size for a field owned by one of an object's ancestor superclasses + * in this JVM. + * + * @return the quantum field size for a superclass field. + */ + int getSuperclassFieldPadding(); + } + + private static class CurrentLayout { + private static final MemoryLayoutSpecification SPEC = + getEffectiveMemoryLayoutSpecification(); + } + + /** + * Given an object, returns the total allocated size, in bytes, of the object + * and all other objects reachable from it. Attempts to to detect the current JVM memory layout, + * but may fail with {@link UnsupportedOperationException}; + * + * @param obj the object; can be null. Passing in a {@link Class} object doesn't do + * anything special, it measures the size of all objects + * reachable through it (which will include its class loader, and by + * extension, all other Class objects loaded by + * the same loader, and all the parent class loaders). It doesn't provide the + * size of the static fields in the JVM class that the Class object + * represents. + * @return the total allocated size of the object and all other objects it + * retains. + * @throws UnsupportedOperationException if the current vm memory layout cannot be detected. + */ + public static long getObjectSize(Object obj) throws UnsupportedOperationException { + return obj == null ? 0 : new ObjectSizeCalculator(CurrentLayout.SPEC).calculateObjectSize(obj); + } + + // Fixed object header size for arrays. + private final int arrayHeaderSize; + // Fixed object header size for non-array objects. + private final int objectHeaderSize; + // Padding for the object size - if the object size is not an exact multiple + // of this, it is padded to the next multiple. + private final int objectPadding; + // Size of reference (pointer) fields. + private final int referenceSize; + // Padding for the fields of superclass before fields of subclasses are + // added. + private final int superclassFieldPadding; + + private final LoadingCache, ClassSizeInfo> classSizeInfos = + CacheBuilder.newBuilder().build(new CacheLoader, ClassSizeInfo>() { + public ClassSizeInfo load(Class clazz) { + return new ClassSizeInfo(clazz); + } + }); + + + private final Set alreadyVisited = Sets.newIdentityHashSet(); + private final Deque pending = new ArrayDeque(16 * 1024); + private long size; + + /** + * Creates an object size calculator that can calculate object sizes for a given + * {@code memoryLayoutSpecification}. + * + * @param memoryLayoutSpecification a description of the JVM memory layout. + */ + public ObjectSizeCalculator(MemoryLayoutSpecification memoryLayoutSpecification) { + Preconditions.checkNotNull(memoryLayoutSpecification); + arrayHeaderSize = memoryLayoutSpecification.getArrayHeaderSize(); + objectHeaderSize = memoryLayoutSpecification.getObjectHeaderSize(); + objectPadding = memoryLayoutSpecification.getObjectPadding(); + referenceSize = memoryLayoutSpecification.getReferenceSize(); + superclassFieldPadding = memoryLayoutSpecification.getSuperclassFieldPadding(); + } + + /** + * Given an object, returns the total allocated size, in bytes, of the object + * and all other objects reachable from it. + * + * @param obj the object; can be null. Passing in a {@link Class} object doesn't do + * anything special, it measures the size of all objects + * reachable through it (which will include its class loader, and by + * extension, all other Class objects loaded by + * the same loader, and all the parent class loaders). It doesn't provide the + * size of the static fields in the JVM class that the Class object + * represents. + * @return the total allocated size of the object and all other objects it + * retains. + */ + public synchronized long calculateObjectSize(Object obj) { + // Breadth-first traversal instead of naive depth-first with recursive + // implementation, so we don't blow the stack traversing long linked lists. + try { + for (; ; ) { + visit(obj); + if (pending.isEmpty()) { + return size; + } + obj = pending.removeFirst(); + } + } finally { + alreadyVisited.clear(); + pending.clear(); + size = 0; + } + } + + private void visit(Object obj) { + if (alreadyVisited.contains(obj)) { + return; + } + final Class clazz = obj.getClass(); + if (clazz == ArrayElementsVisitor.class) { + ((ArrayElementsVisitor) obj).visit(this); + } else { + alreadyVisited.add(obj); + if (clazz.isArray()) { + visitArray(obj); + } else { + classSizeInfos.getUnchecked(clazz).visit(obj, this); + } + } + } + + private void visitArray(Object array) { + final Class componentType = array.getClass().getComponentType(); + final int length = Array.getLength(array); + if (componentType.isPrimitive()) { + increaseByArraySize(length, getPrimitiveFieldSize(componentType)); + } else { + increaseByArraySize(length, referenceSize); + // If we didn't use an ArrayElementsVisitor, we would be enqueueing every + // element of the array here instead. For large arrays, it would + // tremendously enlarge the queue. In essence, we're compressing it into + // a small command object instead. This is different than immediately + // visiting the elements, as their visiting is scheduled for the end of + // the current queue. + switch (length) { + case 0: { + break; + } + case 1: { + enqueue(Array.get(array, 0)); + break; + } + default: { + enqueue(new ArrayElementsVisitor((Object[]) array)); + } + } + } + } + + private void increaseByArraySize(int length, long elementSize) { + increaseSize(roundTo(arrayHeaderSize + length * elementSize, objectPadding)); + } + + private static class ArrayElementsVisitor { + private final Object[] array; + + ArrayElementsVisitor(Object[] array) { + this.array = array; + } + + public void visit(ObjectSizeCalculator calc) { + for (Object elem : array) { + if (elem != null) { + calc.visit(elem); + } + } + } + } + + void enqueue(Object obj) { + if (obj != null) { + pending.addLast(obj); + } + } + + void increaseSize(long objectSize) { + size += objectSize; + } + + @VisibleForTesting + static long roundTo(long x, int multiple) { + return ((x + multiple - 1) / multiple) * multiple; + } + + private class ClassSizeInfo { + // Padded fields + header size + private final long objectSize; + // Only the fields size - used to calculate the subclasses' memory + // footprint. + private final long fieldsSize; + private final Field[] referenceFields; + + public ClassSizeInfo(Class clazz) { + long fieldsSize = 0; + final List referenceFields = new LinkedList(); + for (Field f : clazz.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) { + continue; + } + final Class type = f.getType(); + if (type.isPrimitive()) { + fieldsSize += getPrimitiveFieldSize(type); + } else { + f.setAccessible(true); + referenceFields.add(f); + fieldsSize += referenceSize; + } + } + final Class superClass = clazz.getSuperclass(); + if (superClass != null) { + final ClassSizeInfo superClassInfo = classSizeInfos.getUnchecked(superClass); + fieldsSize += roundTo(superClassInfo.fieldsSize, superclassFieldPadding); + referenceFields.addAll(Arrays.asList(superClassInfo.referenceFields)); + } + this.fieldsSize = fieldsSize; + this.objectSize = roundTo(objectHeaderSize + fieldsSize, objectPadding); + this.referenceFields = referenceFields.toArray( + new Field[referenceFields.size()]); + } + + void visit(Object obj, ObjectSizeCalculator calc) { + calc.increaseSize(objectSize); + enqueueReferencedObjects(obj, calc); + } + + public void enqueueReferencedObjects(Object obj, ObjectSizeCalculator calc) { + for (Field f : referenceFields) { + try { + calc.enqueue(f.get(obj)); + } catch (IllegalAccessException e) { + final AssertionError ae = new AssertionError( + "Unexpected denial of access to " + f, e); + throw ae; + } + } + } + } + + private static long getPrimitiveFieldSize(Class type) { + if (type == boolean.class || type == byte.class) { + return 1; + } + if (type == char.class || type == short.class) { + return 2; + } + if (type == int.class || type == float.class) { + return 4; + } + if (type == long.class || type == double.class) { + return 8; + } + throw new AssertionError("Encountered unexpected primitive type " + + type.getName()); + } + + @VisibleForTesting + static MemoryLayoutSpecification getEffectiveMemoryLayoutSpecification() { + final String vmName = System.getProperty("java.vm.name"); + if (vmName == null || !(vmName.startsWith("Java HotSpot(TM) ") || vmName.startsWith("OpenJDK"))) { + throw new UnsupportedOperationException( + "ObjectSizeCalculator only supported on HotSpot VM"); + } + + final String dataModel = System.getProperty("sun.arch.data.model"); + if ("32".equals(dataModel)) { + // Running with 32-bit data model + return new MemoryLayoutSpecification() { + @Override + public int getArrayHeaderSize() { + return 12; + } + + @Override + public int getObjectHeaderSize() { + return 8; + } + + @Override + public int getObjectPadding() { + return 8; + } + + @Override + public int getReferenceSize() { + return 4; + } + + @Override + public int getSuperclassFieldPadding() { + return 4; + } + }; + } else if (!"64".equals(dataModel)) { + throw new UnsupportedOperationException("Unrecognized value '" + + dataModel + "' of sun.arch.data.model system property"); + } + + final String strVmVersion = System.getProperty("java.vm.version"); + final int vmVersion = Integer.parseInt(strVmVersion.substring(0, + strVmVersion.indexOf('.'))); + if (vmVersion >= 17) { + long maxMemory = 0; + for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) { + maxMemory += mp.getUsage().getMax(); + } + if (maxMemory < 30L * 1024 * 1024 * 1024) { + // HotSpot 17.0 and above use compressed OOPs below 30GB of RAM total + // for all memory pools (yes, including code cache). + return new MemoryLayoutSpecification() { + @Override + public int getArrayHeaderSize() { + return 16; + } + + @Override + public int getObjectHeaderSize() { + return 12; + } + + @Override + public int getObjectPadding() { + return 8; + } + + @Override + public int getReferenceSize() { + return 4; + } + + @Override + public int getSuperclassFieldPadding() { + return 4; + } + }; + } + } + + // In other cases, it's a 64-bit uncompressed OOPs object model + return new MemoryLayoutSpecification() { + @Override + public int getArrayHeaderSize() { + return 24; + } + + @Override + public int getObjectHeaderSize() { + return 16; + } + + @Override + public int getObjectPadding() { + return 8; + } + + @Override + public int getReferenceSize() { + return 8; + } + + @Override + public int getSuperclassFieldPadding() { + return 8; + } + }; + } +} \ No newline at end of file diff --git a/src/test/java/org/xbib/time/schedule/ReadmeTest.java b/src/test/java/org/xbib/time/schedule/ReadmeTest.java new file mode 100644 index 0000000..369d54b --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/ReadmeTest.java @@ -0,0 +1,38 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +/** + * Examples used in the Readme file. + */ +public class ReadmeTest { + + @Test + public void normal() { + ZonedDateTime time = ZonedDateTime.now().withDayOfYear(1).truncatedTo(ChronoUnit.DAYS); + assertTrue(CronExpression.parse("0 0 1 1 *").matches(time)); + assertTrue(CronExpression.parse("@yearly").matches(time)); + assertTrue(CronExpression.parse("@annually").matches(time)); + assertTrue(CronExpression.yearly().matches(time)); + } + + @Test + public void quartzLike() { + CronExpression expression = CronExpression.parser() + .withSecondsField(true) + .withOneBasedDayOfWeek(true) + .allowBothDayFields(false) + .parse("0 15 10 L * ?"); + ZonedDateTime time = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS) + .withYear(2013) + .withMonth(1) + .withDayOfMonth(31) + .withHour(10) + .withMinute(15); + assertTrue(expression.matches(time)); + } + +} diff --git a/src/test/java/org/xbib/time/schedule/Times.java b/src/test/java/org/xbib/time/schedule/Times.java new file mode 100644 index 0000000..c3c13e4 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/Times.java @@ -0,0 +1,102 @@ +package org.xbib.time.schedule; + +import com.google.common.collect.ImmutableSortedSet; +import java.time.DayOfWeek; +import java.time.Month; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.Iterator; +import java.util.NavigableSet; + +public class Times { + + final Integers + seconds, + minutes, + hours, + months, + daysOfWeek, + years, + daysOfMonth; + + Times() { + seconds = new Integers(); + minutes = new Integers(); + hours = new Integers(); + months = new Integers(); + daysOfWeek = new Integers(); + years = new Integers(); + daysOfMonth = new Integers(); + } + + NavigableSet dateTimes() { + if (seconds.isEmpty()) { + seconds.withRange(0, 1); + } + if (minutes.isEmpty()) { + minutes.withRange(0, 1); + } + if (hours.isEmpty()) { + hours.withRange(0, 1); + } + if (months.isEmpty()) { + months.withRange(1, 2); + } + if (years.isEmpty()) { + int thisYear = ZonedDateTime.now().getYear(); + years.withRange(thisYear, thisYear + 1); + } + ImmutableSortedSet.Builder builder = ImmutableSortedSet.naturalOrder(); + for (int second : seconds) { + for (int minute : minutes) { + for (int hour : hours) { + for (int month : months) { + for (int year : years) { + ZonedDateTime base = ZonedDateTime.now() + .truncatedTo(ChronoUnit.DAYS) + .withSecond(second) + .withMinute(minute) + .withHour(hour) + .withMonth(month) + .withDayOfMonth(1) + .withYear(year); + if (!daysOfWeek.isEmpty() && !daysOfMonth.isEmpty()) { + addDaysOfWeek(builder, base); + addDaysOfMonth(builder, base); + } else if (!daysOfWeek.isEmpty()) { + addDaysOfWeek(builder, base); + } else if (!daysOfMonth.isEmpty()) { + addDaysOfMonth(builder, base); + } else { + builder.add(base); + } + } + } + } + } + } + return builder.build(); + } + + private void addDaysOfWeek(ImmutableSortedSet.Builder builder, ZonedDateTime base) { + Month month = base.getMonth(); + Iterator iterator = daysOfWeek.iterator(); + base = base.with(DayOfWeek.of(iterator.next())); + if (base.getMonth() != month) { + base = base.plusWeeks(1); + } + do { + builder.add(base); + base = base.plusWeeks(1); + } while (base.getMonth() == month); + } + + private void addDaysOfMonth(ImmutableSortedSet.Builder builder, ZonedDateTime base) { + for (int day : daysOfMonth) { + if (day <= base.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth()) { + builder.add(base.withDayOfMonth(day)); + } + } + } +} diff --git a/src/test/java/org/xbib/time/schedule/TokensTest.java b/src/test/java/org/xbib/time/schedule/TokensTest.java new file mode 100644 index 0000000..86493db --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/TokensTest.java @@ -0,0 +1,204 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import org.junit.Test; + +public class TokensTest { + private Tokens tokens; + + @Test + public void empty() { + tokenize(""); + assertFalse(tokens.hasNext()); + assertEndOfInput(); + } + + @Test + public void end() { + tokenize("1"); + assertNextIsNumber(1); + assertFalse(tokens.hasNext()); + assertEndOfInput(); + } + + @Test + public void offset() { + tokenize("5,6"); + tokens.offset(5); + assertNextIsNumber(0); + assertNextIs(Token.VALUE_SEPARATOR); + assertNextIsNumber(1); + assertEndOfInput(); + } + + @Test + public void resetClearsOffset() { + tokenize("2,2"); + tokens.offset(1); + assertNextIsNumber(1); + assertNextIs(Token.VALUE_SEPARATOR); + tokens.reset(); + assertNextIsNumber(2); + } + + @Test + public void resetClearsKeywords() { + tokenize("FRI,FRI"); + tokens.keywords(DayOfWeekField.Builder.KEYWORDS); + assertNextIsNumber(5); + assertNextIs(Token.VALUE_SEPARATOR); + tokens.reset(); + try { + tokens.next(); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Bad keyword 'FRI' at position 4 in string: FRI,FRI", e.getMessage()); + } + } + + @Test + public void matchOne() { + tokenize("?"); + assertNextIs(Token.MATCH_ONE); + assertEndOfInput(); + } + + @Test + public void matchAll() { + tokenize("*"); + assertNextIs(Token.MATCH_ALL); + assertEndOfInput(); + } + + @Test + public void skip() { + tokenize("/"); + assertNextIs(Token.SKIP); + assertEndOfInput(); + } + + @Test + public void range() { + tokenize("-"); + assertNextIs(Token.RANGE); + assertEndOfInput(); + } + + @Test + public void last() { + tokenize("1L"); + assertNextIsNumber(1); + assertNextIs(Token.LAST); + assertEndOfInput(); + } + + @Test + public void lastAlone() { + tokenize("L"); + assertNextIs(Token.LAST); + assertEndOfInput(); + } + + @Test + public void weekday() { + tokenize("1W"); + assertNextIsNumber(1); + assertNextIs(Token.WEEKDAY); + assertEndOfInput(); + } + + @Test + public void nth() { + tokenize("1#2"); + assertNextIsNumber(1); + assertNextIs(Token.NTH); + assertNextIsNumber(2); + assertEndOfInput(); + } + + @Test + public void multipleWhitespaceCharacters() { + tokenize(" \t \t \t \t "); + assertEquals(Token.FIELD_SEPARATOR, tokens.next()); + assertEndOfInput(); + } + + @Test + public void keywordRange() { + tokenize("MON-FRI"); + tokens.keywords(DayOfWeekField.Builder.KEYWORDS); + assertEquals(Token.NUMBER, tokens.next()); + assertEquals(1, tokens.number()); + assertEquals(Token.RANGE, tokens.next()); + assertEquals(Token.NUMBER, tokens.next()); + assertEquals(5, tokens.number()); + assertEndOfInput(); + } + + @Test + public void badCharacter() { + tokenize("5%"); + assertEquals(Token.NUMBER, tokens.next()); + try { + tokens.next(); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Bad character '%' at position 1 in string: 5%", e.getMessage()); + } + } + + @Test + public void badLetter() { + tokenize("1F"); + assertEquals(Token.NUMBER, tokens.next()); + try { + tokens.next(); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Bad character 'F' at position 1 in string: 1F", e.getMessage()); + } + } + + @Test + public void badKeywordOfValidLength() { + tokenize("ABC"); + tokens.keywords(DayOfWeekField.Builder.KEYWORDS); + try { + tokens.next(); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Bad keyword 'ABC' at position 0 in string: ABC", e.getMessage()); + } + } + + @Test + public void badKeywordOfInvalidLength() { + tokenize("AB"); + tokens.keywords(DayOfWeekField.Builder.KEYWORDS); + try { + tokens.next(); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertEquals("Bad keyword 'AB' at position 0 in string: AB", e.getMessage()); + } + } + + private void assertEndOfInput() { + assertNextIs(Token.END_OF_INPUT); + } + + private void assertNextIsNumber(int expected) { + assertNextIs(Token.NUMBER); + assertEquals(expected, tokens.number()); + } + + private void assertNextIs(Token expected) { + assertEquals(expected, tokens.next()); + } + + private void tokenize(String s) { + tokens = new Tokens(s); + } +} diff --git a/src/test/java/org/xbib/time/schedule/WhatQuartzDoesNotSupport.java b/src/test/java/org/xbib/time/schedule/WhatQuartzDoesNotSupport.java new file mode 100644 index 0000000..af33881 --- /dev/null +++ b/src/test/java/org/xbib/time/schedule/WhatQuartzDoesNotSupport.java @@ -0,0 +1,53 @@ +package org.xbib.time.schedule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.xbib.time.schedule.DateTimes.nthOfMonth; +import org.junit.Test; +import java.text.ParseException; +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class WhatQuartzDoesNotSupport { + @Test + public void multipleNthDayOfWeek() { + try { + org.quartz.CronExpression quartz = new org.quartz.CronExpression("0 0 0 ? * 6#3,4#1,3#2"); + List times = new ArrayList<>(); + ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); + int year = t.getYear(); + while (t.getYear() == year) { + times.add(nthOfMonth(t, DayOfWeek.FRIDAY, 3)); + times.add(nthOfMonth(t, DayOfWeek.TUESDAY, 2)); + t = t.plusMonths(1); + } + for (ZonedDateTime time : times) { + boolean satisfied = quartz.isSatisfiedBy(Date.from(time.toInstant())); + if (time.getDayOfWeek() == DayOfWeek.TUESDAY) { + // Earlier versions of Quartz only picked up the last one + assertTrue(satisfied); + } else { + assertFalse(satisfied); + } + } + } catch (ParseException e) { + assertEquals("Support for specifying multiple \"nth\" days is not implemented.", e.getMessage()); + } + } + + @Test + public void multipleLastDayOfWeek() throws Exception { + try { + new org.quartz.CronExpression("0 0 0 ? * 6L,4L,3L"); + fail("Expected exception"); + } catch (ParseException e) { + assertEquals("Support for specifying 'L' with other days of the week is not implemented", e.getMessage()); + } + } +} diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml deleted file mode 100644 index f71aced..0000000 --- a/src/test/resources/log4j2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file