initial commit

This commit is contained in:
Jörg Prante 2016-11-02 11:46:35 +01:00
commit 2aead18a0f
199 changed files with 20184 additions and 0 deletions

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
/data
/work
/logs
/.idea
/target
.DS_Store
*.iml
/.settings
/.classpath
/.project
/.gradle
build
/plugins
/sessions
*~
*.MARC

202
LICENSE.txt Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

79
build.gradle Normal file
View file

@ -0,0 +1,79 @@
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.1.0"
}
group = 'org.xbib'
version = '1.0.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)
}
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'
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:all" << "-profile" << "compact2"
}
test {
testLogging {
showStandardStreams = false
exceptionFormat = 'full'
}
systemProperty 'java.util.logging.manager', 'org.apache.logging.log4j.jul.LogManager'
}
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
}
}
apply from: 'gradle/ext.gradle'
apply from: 'gradle/publish.gradle'
apply from: 'gradle/sonarqube.gradle'

View file

@ -0,0 +1,323 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<!-- This is a checkstyle configuration file. For descriptions of
what the following rules do, please see the checkstyle configuration
page at http://checkstyle.sourceforge.net/config.html -->
<module name="Checker">
<module name="FileTabCharacter">
<!-- Checks that there are no tab characters in the file.
-->
</module>
<module name="NewlineAtEndOfFile">
<property name="lineSeparator" value="lf"/>
</module>
<module name="RegexpSingleline">
<!-- Checks that FIXME is not used in comments. TODO is preferred.
-->
<property name="format" value="((//.*)|(\*.*))FIXME" />
<property name="message" value='TODO is preferred to FIXME. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="RegexpSingleline">
<!-- Checks that TODOs are named. (Actually, just that they are followed
by an open paren.)
-->
<property name="format" value="((//.*)|(\*.*))TODO[^(]" />
<property name="message" value='All TODOs should be named. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="JavadocPackage">
<!-- Checks that each Java package has a Javadoc file used for commenting.
Only allows a package-info.java, not package.html. -->
</module>
<!-- All Java AST specific tests live under TreeWalker module. -->
<module name="TreeWalker">
<!--
IMPORT CHECKS
-->
<module name="RedundantImport">
<!-- Checks for redundant import statements. -->
<property name="severity" value="error"/>
</module>
<module name="ImportOrder">
<!-- Checks for out of order import statements. -->
<property name="severity" value="warning"/>
<property name="groups" value="com,junit,net,org,java,javax"/>
<!-- This ensures that static imports go first. -->
<property name="option" value="top"/>
<property name="tokens" value="STATIC_IMPORT, IMPORT"/>
</module>
<!--
JAVADOC CHECKS
-->
<!-- Checks for Javadoc comments. -->
<!-- See http://checkstyle.sf.net/config_javadoc.html -->
<module name="JavadocMethod">
<property name="scope" value="protected"/>
<property name="severity" value="warning"/>
<property name="allowMissingJavadoc" value="true"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowThrowsTagsForSubclasses" value="true"/>
<property name="allowUndeclaredRTE" value="true"/>
</module>
<module name="JavadocType">
<property name="scope" value="protected"/>
<property name="severity" value="error"/>
</module>
<module name="JavadocStyle">
<property name="severity" value="warning"/>
</module>
<!--
NAMING CHECKS
-->
<!-- Item 38 - Adhere to generally accepted naming conventions -->
<module name="PackageName">
<!-- Validates identifiers for package names against the
supplied expression. -->
<!-- Here the default checkstyle rule restricts package name parts to
seven characters, this is not in line with common practice at Google.
-->
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]{1,})*$"/>
<property name="severity" value="warning"/>
</module>
<module name="TypeNameCheck">
<!-- Validates static, final fields against the
expression "^[A-Z][a-zA-Z0-9]*$". -->
<metadata name="altname" value="TypeName"/>
<property name="severity" value="warning"/>
</module>
<module name="ConstantNameCheck">
<!-- Validates non-private, static, final fields against the supplied
public/package final fields "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$". -->
<metadata name="altname" value="ConstantName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="false"/>
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|FLAG_.*)$"/>
<message key="name.invalidPattern"
value="Variable ''{0}'' should be in ALL_CAPS (if it is a constant) or be private (otherwise)."/>
<property name="severity" value="warning"/>
</module>
<module name="StaticVariableNameCheck">
<!-- Validates static, non-final fields against the supplied
expression "^[a-z][a-zA-Z0-9]*_?$". -->
<metadata name="altname" value="StaticVariableName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*_?$"/>
<property name="severity" value="warning"/>
</module>
<module name="MemberNameCheck">
<!-- Validates non-static members against the supplied expression. -->
<metadata name="altname" value="MemberName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="severity" value="warning"/>
</module>
<module name="MethodNameCheck">
<!-- Validates identifiers for method names. -->
<metadata name="altname" value="MethodName"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$"/>
<property name="severity" value="warning"/>
</module>
<module name="ParameterName">
<!-- Validates identifiers for method parameters against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalFinalVariableName">
<!-- Validates identifiers for local final variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalVariableName">
<!-- Validates identifiers for local variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<!--
LENGTH and CODING CHECKS
-->
<module name="LineLength">
<!-- Checks if a line is too long. -->
<property name="max" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}" default="128"/>
<property name="severity" value="error"/>
<!--
The default ignore pattern exempts the following elements:
- import statements
- long URLs inside comments
-->
<property name="ignorePattern"
value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}"
default="^(package .*;\s*)|(import .*;\s*)|( *(\*|//).*https?://.*)$"/>
</module>
<module name="LeftCurly">
<!-- Checks for placement of the left curly brace ('{'). -->
<property name="severity" value="warning"/>
</module>
<module name="RightCurly">
<!-- Checks right curlies on CATCH, ELSE, and TRY blocks are on
the same line. e.g., the following example is fine:
<pre>
if {
...
} else
</pre>
-->
<!-- This next example is not fine:
<pre>
if {
...
}
else
</pre>
-->
<property name="option" value="same"/>
<property name="severity" value="warning"/>
</module>
<!-- Checks for braces around if and else blocks -->
<module name="NeedBraces">
<property name="severity" value="warning"/>
<property name="tokens" value="LITERAL_IF, LITERAL_ELSE, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO"/>
</module>
<module name="UpperEll">
<!-- Checks that long constants are defined with an upper ell.-->
<property name="severity" value="error"/>
</module>
<module name="FallThrough">
<!-- Warn about falling through to the next case statement. Similar to
javac -Xlint:fallthrough, but the check is suppressed if a single-line comment
on the last non-blank line preceding the fallen-into case contains 'fall through' (or
some other variants which we don't publicized to promote consistency).
-->
<property name="reliefPattern"
value="fall through|Fall through|fallthru|Fallthru|falls through|Falls through|fallthrough|Fallthrough|No break|NO break|no break|continue on"/>
<property name="severity" value="error"/>
</module>
<!--
MODIFIERS CHECKS
-->
<module name="ModifierOrder">
<!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and
8.4.3. The prescribed order is:
public, protected, private, abstract, static, final, transient, volatile,
synchronized, native, strictfp
-->
</module>
<!--
WHITESPACE CHECKS
-->
<module name="WhitespaceAround">
<!-- Checks that various tokens are surrounded by whitespace.
This includes most binary operators and keywords followed
by regular or curly braces.
-->
<property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR,
BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,
EQUAL, GE, GT, LAND, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,
SL, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN"/>
<property name="severity" value="error"/>
</module>
<module name="WhitespaceAfter">
<!-- Checks that commas, semicolons and typecasts are followed by
whitespace.
-->
<property name="tokens" value="COMMA, SEMI, TYPECAST"/>
</module>
<module name="NoWhitespaceAfter">
<!-- Checks that there is no whitespace after various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,
UNARY_PLUS"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="NoWhitespaceBefore">
<!-- Checks that there is no whitespace before various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="ParenPad">
<!-- Checks that there is no whitespace before close parens or after
open parens.
-->
<property name="severity" value="warning"/>
</module>
</module>
</module>

8
gradle/ext.gradle Normal file
View file

@ -0,0 +1,8 @@
ext {
user = 'xbib'
projectName = 'time'
projectDescription = 'A collection of date/time methods for Java 8'
scmUrl = 'https://github.com/xbib/time'
scmConnection = 'scm:git:git://github.com/xbib/time.git'
scmDeveloperConnection = 'scm:git:git://github.com/xbib/time.git'
}

73
gradle/publish.gradle Normal file
View file

@ -0,0 +1,73 @@
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'
}
}
}
}
}
}
}
githubPages {
repoUri = 'git@github.com:xbib/marc.git'
targetBranch = "gh-pages"
pages {
from(file('build/jbake')) {
into '.'
}
}
}

41
gradle/sonarqube.gradle Normal file
View file

@ -0,0 +1,41 @@
tasks.withType(FindBugs) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = false
}
}
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
jacocoTestReport {
reports {
xml.enabled true
csv.enabled false
xml.destination "${buildDir}/reports/jacoco-xml"
html.destination "${buildDir}/reports/jacoco-html"
}
}
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/"
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Sun Oct 02 23:57:55 CEST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip

1
settings.gradle Normal file
View file

@ -0,0 +1 @@
rootProject.name = 'time'

View file

@ -0,0 +1,193 @@
package org.xbib.time.chronic;
import org.xbib.time.chronic.handlers.Handler;
import org.xbib.time.chronic.numerizer.Numerizer;
import org.xbib.time.chronic.repeaters.Repeater;
import org.xbib.time.chronic.tags.*;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class Chronic {
private Chronic() {
}
public static Span parse(String text) throws ParseException {
return Chronic.parse(text, new Options());
}
/**
* Parses a string containing a natural language date or time. If the parser
* can find a date or time, either a Time or Chronic::Span will be returned
* (depending on the value of <tt>:guess</tt>). If no date or time can be found,
* +nil+ will be returned.
* <p>
* Options are:
* <p>
* [<tt>:context</tt>]
* <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
* <p>
* If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
* and if an ambiguous string is given, it will assume it is in the
* past. Specify <tt>:future</tt> or omit to set a future context.
* <p>
* [<tt>:now</tt>]
* Time (defaults to Time.now)
* <p>
* By setting <tt>:now</tt> to a Time, all computations will be based off
* of that time instead of Time.now
* <p>
* [<tt>:guess</tt>]
* +true+ or +false+ (defaults to +true+)
* <p>
* 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 <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
* <p>
* [<tt>:ambiguous_time_range</tt>]
* Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
* <p>
* 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 <tt>7</tt>, 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 <tt>:none</tt> is given, no assumption
* will be made, and the first matching instance of that time will
* be used.
* @param text text
* @param options options
* @return span
* @throws ParseException parse exception
*/
@SuppressWarnings("unchecked")
public static Span parse(String text, Options options) throws ParseException {
String normalizedText = Chronic.preNormalize(text);
List<Token> tokens = Chronic.baseTokenize(normalizedText);
List<Class<?>> optionScannerClasses = new LinkedList<>();
optionScannerClasses.add(Repeater.class);
for (Class<?> optionScannerClass : optionScannerClasses) {
try {
tokens = (List<Token>) optionScannerClass.getMethod("scan", List.class, Options.class)
.invoke(null, tokens, options);
} catch (Throwable e) {
throw new ParseException("failed to scan tokens", 0);
}
}
List<Class<?>> scannerClasses = new LinkedList<>();
scannerClasses.add(Grabber.class);
scannerClasses.add(Pointer.class);
scannerClasses.add(Scalar.class);
scannerClasses.add(Ordinal.class);
scannerClasses.add(Separator.class);
scannerClasses.add(TimeZone.class);
for (Class<?> scannerClass : scannerClasses) {
try {
tokens = (List<Token>) scannerClass.getMethod("scan", List.class, Options.class)
.invoke(null, tokens, options);
} catch (Throwable e) {
throw new ParseException("failed to scan tokens", 0);
}
}
List<Token> taggedTokens = new LinkedList<>();
for (Token token : tokens) {
if (token.isTagged()) {
taggedTokens.add(token);
}
}
tokens = taggedTokens;
Span span = Handler.tokensToSpan(tokens, options);
// guess a time within a span if required
if (options.isGuess()) {
span = guess(span);
}
return span;
}
/**
* Clean up the specified input text by stripping unwanted characters,
* converting idioms to their canonical form, converting number words
* to numbers (three =&gt; 3), and converting ordinal words to numeric
* ordinals (third =&gt; 3rd).
* @param text text
* @return string
*/
protected static String preNormalize(String text) {
String normalizedText = text.toLowerCase();
normalizedText = Chronic.numericizeNumbers(normalizedText);
normalizedText = normalizedText.replaceAll("['\"\\.]", "");
normalizedText = normalizedText.replaceAll("([/\\-,@])", " $1 ");
normalizedText = normalizedText.replaceAll("\\btoday\\b", "this day");
normalizedText = normalizedText.replaceAll("\\btomm?orr?ow\\b", "next day");
normalizedText = normalizedText.replaceAll("\\byesterday\\b", "last day");
normalizedText = normalizedText.replaceAll("\\bnoon\\b", "12:00");
normalizedText = normalizedText.replaceAll("\\bmidnight\\b", "24:00");
normalizedText = normalizedText.replaceAll("\\bbefore now\\b", "past");
normalizedText = normalizedText.replaceAll("\\bnow\\b", "this second");
normalizedText = normalizedText.replaceAll("\\b(ago|before)\\b", "past");
normalizedText = normalizedText.replaceAll("\\bthis past\\b", "last");
normalizedText = normalizedText.replaceAll("\\bthis last\\b", "last");
normalizedText = normalizedText.replaceAll("\\b(?:in|during) the (morning)\\b", "$1");
normalizedText = normalizedText.replaceAll("\\b(?:in the|during the|at) (afternoon|evening|night)\\b", "$1");
normalizedText = normalizedText.replaceAll("\\btonight\\b", "this night");
normalizedText = normalizedText.replaceAll("(?=\\w)([ap]m|oclock)\\b", " $1");
normalizedText = normalizedText.replaceAll("\\b(hence|after|from)\\b", "future");
normalizedText = Chronic.numericizeOrdinals(normalizedText);
return normalizedText;
}
/**
* Convert number words to numbers (three =&gt; 3).
* @param text text
* @return string
*/
protected static String numericizeNumbers(String text) {
return Numerizer.numerize(text);
}
/**
* Convert ordinal words to numeric ordinals (third =&gt; 3rd).
* @param text text
* @return string
*/
protected static String numericizeOrdinals(String text) {
return text;
}
/**
* Split the text on spaces and convert each word into
* a Token.
* @param text text
* @return list of tokens
*/
protected static List<Token> baseTokenize(String text) {
String[] words = text.split(" ");
List<Token> tokens = new LinkedList<>();
for (String word : words) {
tokens.add(new Token(word));
}
return tokens;
}
/**
* Guess a specific time within the given span.
* @param span span
* @return span
*/
protected static Span guess(Span span) {
if (span == null) {
return null;
}
Long guessValue;
if (span.getWidth() > 1) {
guessValue = span.getBegin() + (span.getWidth() / 2);
} else {
guessValue = span.getBegin();
}
return new Span(guessValue, guessValue, span.getZoneId());
}
}

View file

@ -0,0 +1,81 @@
package org.xbib.time.chronic;
import org.xbib.time.chronic.tags.Pointer;
import java.time.ZoneId;
import java.time.ZonedDateTime;
/**
*
*/
public class Options {
private ZonedDateTime now;
private ZoneId zoneId;
private Pointer.PointerType context;
private boolean guess;
private int ambiguousTimeRange;
private boolean compatibilityMode;
public Options() {
setContext(Pointer.PointerType.FUTURE);
setNow(ZonedDateTime.now());
setZoneId(ZoneId.of("GMT"));
setGuess(true);
setAmbiguousTimeRange(6);
}
public boolean isCompatibilityMode() {
return compatibilityMode;
}
public Options setCompatibilityMode(boolean compatibilityMode) {
this.compatibilityMode = compatibilityMode;
return this;
}
public Pointer.PointerType getContext() {
return context;
}
public Options setContext(Pointer.PointerType context) {
this.context = context;
return this;
}
public ZonedDateTime getNow() {
return now;
}
public Options setNow(ZonedDateTime now) {
this.now = now;
return this;
}
public ZoneId getZoneId() {
return zoneId;
}
public Options setZoneId(ZoneId zoneId) {
this.zoneId = zoneId;
return this;
}
public boolean isGuess() {
return guess;
}
public Options setGuess(boolean guess) {
this.guess = guess;
return this;
}
public int getAmbiguousTimeRange() {
return ambiguousTimeRange;
}
public Options setAmbiguousTimeRange(int ambiguousTimeRange) {
this.ambiguousTimeRange = ambiguousTimeRange;
return this;
}
}

View file

@ -0,0 +1,40 @@
package org.xbib.time.chronic;
/**
*
*/
public class Range {
private Long begin;
private Long end;
public Range(long begin, long end) {
this.begin = begin;
this.end = end;
}
public Long getBegin() {
return begin;
}
public Long getEnd() {
return end;
}
public Long getWidth() {
return getEnd() - getBegin();
}
public boolean contains(long value) {
return begin <= value && end >= value;
}
@Override
public int hashCode() {
return begin.hashCode() * end.hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof Range && ((Range) obj).begin.equals(begin) && ((Range) obj).end.equals(end);
}
}

View file

@ -0,0 +1,54 @@
package org.xbib.time.chronic;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalUnit;
/**
*
*/
public class Span extends Range {
private final ZoneId zoneId;
public Span(ZonedDateTime begin, TemporalUnit field, int amount) {
this(begin, begin.plus(amount, field));
}
public Span(ZonedDateTime begin, ZonedDateTime end) {
this(begin.toInstant(), end.toInstant(), begin.getZone());
}
public Span(Instant begin, Instant end, ZoneId zoneId) {
this(begin.getEpochSecond(), end.getEpochSecond(), zoneId);
}
public Span(long begin, long end, ZoneId zoneId) {
super(begin, end);
this.zoneId = zoneId;
}
public ZonedDateTime getBeginCalendar() {
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(getBegin()), zoneId);
}
public ZonedDateTime getEndCalendar() {
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(getEnd()), zoneId);
}
public Span add(long seconds) {
return new Span(getBegin() + seconds, getEnd() + seconds, zoneId);
}
public ZoneId getZoneId() {
return zoneId;
}
@Override
public String toString() {
return "(" + DateTimeFormatter.ISO_INSTANT.format(getBeginCalendar())
+ ".." + DateTimeFormatter.ISO_INSTANT.format(getEndCalendar()) + ")";
}
}

View file

@ -0,0 +1,35 @@
package org.xbib.time.chronic;
/**
*
*/
public class Tick {
private int time;
private boolean ambiguous;
public Tick(int time, boolean ambiguous) {
this.time = time;
this.ambiguous = ambiguous;
}
public boolean isAmbiguous() {
return ambiguous;
}
public void setTime(int time) {
this.time = time;
}
public Tick times(int other) {
return new Tick(time * other, ambiguous);
}
public int intValue() {
return time;
}
@Override
public String toString() {
return time + (ambiguous ? "?" : "");
}
}

View file

@ -0,0 +1,97 @@
package org.xbib.time.chronic;
import org.xbib.time.chronic.tags.Tag;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class Token {
private String word;
private List<Tag<?>> tags;
public Token(String word) {
this.word = word;
tags = new LinkedList<>();
}
public String getWord() {
return word;
}
/**
* Tag this token with the specified tag.
* @param newTag new tag
*/
public void tag(Tag<?> newTag) {
tags.add(newTag);
}
/**
* Remove all tags of the given class.
* @param tagClass tag class
*/
public void untag(Class<?> tagClass) {
Iterator<Tag<?>> tagIter = tags.iterator();
while (tagIter.hasNext()) {
Tag<?> tag = tagIter.next();
if (tagClass.isInstance(tag)) {
tagIter.remove();
}
}
}
/**
* Return true if this token has any tags.
* @return true if token has tags
*/
public boolean isTagged() {
return !tags.isEmpty();
}
/**
* Return the Tag that matches the given class.
* @param tagClass tag class
* @param <T> type parameter
* @return tag
*/
@SuppressWarnings("unchecked")
public <T extends Tag<?>> T getTag(Class<T> tagClass) {
List<T> matches = getTags(tagClass);
T matchingTag = null;
if (matches.size() > 0) {
matchingTag = matches.get(0);
}
return matchingTag;
}
public List<Tag<?>> getTags() {
return tags;
}
/**
* Return the Tag that matches the given class.
*
* @param tagClass tag class
* @param <T> type parameter
* @return list of tags
*/
@SuppressWarnings("unchecked")
public <T extends Tag<?>> List<T> getTags(Class<T> tagClass) {
List<T> matches = new LinkedList<>();
for (Tag<?> tag : tags) {
if (tagClass.isInstance(tag)) {
matches.add((T) tag);
}
}
return matches;
}
@Override
public String toString() {
return word + " " + tags;
}
}

View file

@ -0,0 +1,386 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.EnumRepeaterDayPortion;
import org.xbib.time.chronic.repeaters.IntegerRepeaterDayPortion;
import org.xbib.time.chronic.repeaters.Repeater;
import org.xbib.time.chronic.repeaters.RepeaterDayName;
import org.xbib.time.chronic.repeaters.RepeaterDayPortion;
import org.xbib.time.chronic.repeaters.RepeaterMonthName;
import org.xbib.time.chronic.repeaters.RepeaterTime;
import org.xbib.time.chronic.tags.Grabber;
import org.xbib.time.chronic.tags.Ordinal;
import org.xbib.time.chronic.tags.OrdinalDay;
import org.xbib.time.chronic.tags.Pointer;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import org.xbib.time.chronic.tags.Scalar;
import org.xbib.time.chronic.tags.ScalarDay;
import org.xbib.time.chronic.tags.ScalarMonth;
import org.xbib.time.chronic.tags.ScalarYear;
import org.xbib.time.chronic.tags.Separator;
import org.xbib.time.chronic.tags.SeparatorAt;
import org.xbib.time.chronic.tags.SeparatorComma;
import org.xbib.time.chronic.tags.SeparatorIn;
import org.xbib.time.chronic.tags.SeparatorSlashOrDash;
import org.xbib.time.chronic.tags.Tag;
import org.xbib.time.chronic.tags.TimeZone;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
*
*/
public class Handler {
private static Map<HandlerType, List<Handler>> definitions;
private HandlerPattern[] patterns;
private IHandler handler;
private boolean compatible;
public Handler(IHandler handler, HandlerPattern... patterns) {
this(handler, true, patterns);
}
public Handler(IHandler handler, boolean compatible, HandlerPattern... patterns) {
this.handler = handler;
this.compatible = compatible;
this.patterns = patterns;
}
public static synchronized Map<HandlerType, List<Handler>> definitions() {
if (definitions == null) {
Map<HandlerType, List<Handler>> definitions = new HashMap<>();
List<Handler> timeHandlers = new LinkedList<>();
timeHandlers.add(new Handler(null, new TagPattern(RepeaterTime.class),
new TagPattern(RepeaterDayPortion.class, true)));
definitions.put(HandlerType.TIME, timeHandlers);
List<Handler> dateHandlers = new LinkedList<>();
dateHandlers.add(new Handler(new RdnRmnSdTTzSyHandler(),
new TagPattern(RepeaterDayName.class), new TagPattern(RepeaterMonthName.class),
new TagPattern(ScalarDay.class), new TagPattern(RepeaterTime.class),
new TagPattern(TimeZone.class), new TagPattern(ScalarYear.class)));
// DIFF: We add an optional comma to MDY
dateHandlers.add(new Handler(new RmnSdSyHandler(),
new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class),
new TagPattern(SeparatorComma.class, true), new TagPattern(ScalarYear.class)));
dateHandlers.add(new Handler(new RmnSdSyHandler(),
new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class),
new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true),
new HandlerTypePattern(HandlerType.TIME, true)));
dateHandlers.add(new Handler(new RmnSdHandler(),
new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class),
new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(HandlerType.TIME, true)));
dateHandlers.add(new Handler(new RmnOdHandler(), new TagPattern(RepeaterMonthName.class),
new TagPattern(OrdinalDay.class), new TagPattern(SeparatorAt.class, true),
new HandlerTypePattern(HandlerType.TIME, true)));
dateHandlers.add(new Handler(new RmnSyHandler(), new TagPattern(RepeaterMonthName.class),
new TagPattern(ScalarYear.class)));
dateHandlers.add(new Handler(new SdRmnSyHandler(), new TagPattern(ScalarDay.class),
new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarYear.class),
new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(HandlerType.TIME, true)));
dateHandlers.add(new Handler(new SmSdSyHandler(), new TagPattern(ScalarMonth.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class),
new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(HandlerType.TIME, true)));
dateHandlers.add(new Handler(new SdSmSyHandler(), new TagPattern(ScalarDay.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarMonth.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class),
new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(HandlerType.TIME, true)));
dateHandlers.add(new Handler(new SySmSdHandler(), new TagPattern(ScalarYear.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarMonth.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class),
new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(HandlerType.TIME, true)));
// DIFF: We make 05/06 interpret as month/day before month/year
dateHandlers.add(new Handler(new SmSdHandler(), false, new TagPattern(ScalarMonth.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class)));
dateHandlers.add(new Handler(new SmSyHandler(), new TagPattern(ScalarMonth.class),
new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class)));
definitions.put(HandlerType.DATE, dateHandlers);
// tonight at 7pm
List<Handler> anchorHandlers = new LinkedList<>();
anchorHandlers.add(new Handler(new RHandler(), new TagPattern(Grabber.class, true),
new TagPattern(Repeater.class), new TagPattern(SeparatorAt.class, true),
new TagPattern(Repeater.class, true), new TagPattern(Repeater.class, true)));
anchorHandlers.add(new Handler(new RHandler(), new TagPattern(Grabber.class, true),
new TagPattern(Repeater.class), new TagPattern(Repeater.class),
new TagPattern(SeparatorAt.class, true), new TagPattern(Repeater.class, true),
new TagPattern(Repeater.class, true)));
anchorHandlers.add(new Handler(new RGRHandler(), new TagPattern(Repeater.class),
new TagPattern(Grabber.class), new TagPattern(Repeater.class)));
definitions.put(HandlerType.ANCHOR, anchorHandlers);
// 3 weeks from now, in 2 months
List<Handler> arrowHandlers = new LinkedList<>();
arrowHandlers.add(new Handler(new SRPHandler(), new TagPattern(Scalar.class),
new TagPattern(Repeater.class), new TagPattern(Pointer.class)));
arrowHandlers.add(new Handler(new PSRHandler(), new TagPattern(Pointer.class),
new TagPattern(Scalar.class), new TagPattern(Repeater.class)));
arrowHandlers.add(new Handler(new SRPAHandler(), new TagPattern(Scalar.class),
new TagPattern(Repeater.class), new TagPattern(Pointer.class),
new HandlerTypePattern(HandlerType.ANCHOR)));
definitions.put(HandlerType.ARROW, arrowHandlers);
// 3rd week in march
List<Handler> narrowHandlers = new LinkedList<>();
narrowHandlers.add(new Handler(new ORSRHandler(), new TagPattern(Ordinal.class),
new TagPattern(Repeater.class), new TagPattern(SeparatorIn.class), new TagPattern(Repeater.class)));
narrowHandlers.add(new Handler(new ORGRHandler(), new TagPattern(Ordinal.class),
new TagPattern(Repeater.class), new TagPattern(Grabber.class), new TagPattern(Repeater.class)));
definitions.put(HandlerType.NARROW, narrowHandlers);
Handler.definitions = definitions;
}
return definitions;
}
public static Span tokensToSpan(List<Token> tokens, Options options) throws ParseException {
// maybe it's a specific date
Map<HandlerType, List<Handler>> definitions = definitions();
for (Handler handler : definitions.get(HandlerType.DATE)) {
if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
List<Token> goodTokens = new LinkedList<>();
for (Token token : tokens) {
if (token.getTag(Separator.class) == null) {
goodTokens.add(token);
}
}
return handler.getHandler().handle(goodTokens, options);
}
}
// I guess it's not a specific date, maybe it's just an anchor
for (Handler handler : definitions.get(HandlerType.ANCHOR)) {
if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
List<Token> goodTokens = new LinkedList<>();
for (Token token : tokens) {
if (token.getTag(Separator.class) == null) {
goodTokens.add(token);
}
}
return handler.getHandler().handle(goodTokens, options);
}
}
// not an anchor, perhaps it's an arrow
for (Handler handler : definitions.get(HandlerType.ARROW)) {
if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
List<Token> goodTokens = new LinkedList<>();
for (Token token : tokens) {
if (token.getTag(SeparatorAt.class) == null &&
token.getTag(SeparatorSlashOrDash.class) == null &&
token.getTag(SeparatorComma.class) == null) {
goodTokens.add(token);
}
}
return handler.getHandler().handle(goodTokens, options);
}
}
// not an arrow, let's hope it's a narrow
for (Handler handler : definitions.get(HandlerType.NARROW)) {
if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
return handler.getHandler().handle(tokens, options);
}
}
throw new ParseException("no span found for " + tokens, 0);
}
public static List<Repeater<?>> getRepeaters(List<Token> tokens) {
List<Repeater<?>> repeaters = new LinkedList<>();
for (Token token : tokens) {
Repeater<?> tag = token.getTag(Repeater.class);
if (tag != null) {
repeaters.add(tag);
}
}
Collections.sort(repeaters);
Collections.reverse(repeaters);
return repeaters;
}
public static Span getAnchor(List<Token> tokens, Options options) {
Grabber grabber = new Grabber(Grabber.Relative.THIS);
PointerType pointer = PointerType.FUTURE;
List<Repeater<?>> repeaters = getRepeaters(tokens);
for (int i = 0; i < repeaters.size(); i++) {
tokens.remove(tokens.size() - 1);
}
if (!tokens.isEmpty() && tokens.get(0).getTag(Grabber.class) != null) {
grabber = tokens.get(0).getTag(Grabber.class);
tokens.remove(tokens.size() - 1);
}
Repeater<?> head = repeaters.remove(0);
head.setNow(options.getNow());
Span outerSpan;
Grabber.Relative grabberType = grabber.getType();
if (grabberType == Grabber.Relative.LAST) {
outerSpan = head.nextSpan(PointerType.PAST);
} else if (grabberType == Grabber.Relative.THIS) {
if (repeaters.size() > 0) {
outerSpan = head.thisSpan(PointerType.NONE);
} else {
outerSpan = head.thisSpan(options.getContext());
}
} else if (grabberType == Grabber.Relative.NEXT) {
outerSpan = head.nextSpan(PointerType.FUTURE);
} else {
throw new IllegalArgumentException("Invalid grabber type " + grabberType + ".");
}
return findWithin(repeaters, outerSpan, pointer);
}
public static Span dayOrTime(ZonedDateTime dayStart, List<Token> timeTokens, Options options) {
Span outerSpan = new Span(dayStart, dayStart.plus(1, ChronoUnit.DAYS));
if (!timeTokens.isEmpty()) {
options.setNow(outerSpan.getBeginCalendar());
return getAnchor(dealiasAndDisambiguateTimes(timeTokens, options), options);
}
return outerSpan;
}
/**
* Recursively finds repeaters within other repeaters.
* Returns a Span representing the innermost time span
* or nil if no repeater union could be found
* @param tags tags
* @param span span
* @param pointer pointer
* @return span
*/
public static Span findWithin(List<Repeater<?>> tags, Span span, PointerType pointer) {
if (tags.isEmpty()) {
return span;
}
Repeater<?> head = tags.get(0);
List<Repeater<?>> rest = (tags.size() > 1) ? tags.subList(1, tags.size()) : new LinkedList<>();
head.setNow((pointer == PointerType.FUTURE) ? span.getBeginCalendar() : span.getEndCalendar());
Span h = head.thisSpan(PointerType.NONE);
if (span.contains(h.getBegin()) || span.contains(h.getEnd())) {
return findWithin(rest, h, pointer);
}
return null;
}
@SuppressWarnings("unchecked")
public static List<Token> dealiasAndDisambiguateTimes(List<Token> tokens, Options options) {
// handle aliases of am/pm
// 5:00 in the morning => 5:00 am
// 7:00 in the evening => 7:00 pm
int dayPortionIndex = -1;
int tokenSize = tokens.size();
for (int i = 0; dayPortionIndex == -1 && i < tokenSize; i++) {
Token t = tokens.get(i);
if (t.getTag(RepeaterDayPortion.class) != null) {
dayPortionIndex = i;
}
}
int timeIndex = -1;
for (int i = 0; timeIndex == -1 && i < tokenSize; i++) {
Token t = tokens.get(i);
if (t.getTag(RepeaterTime.class) != null) {
timeIndex = i;
}
}
if (dayPortionIndex != -1 && timeIndex != -1) {
Token t1 = tokens.get(dayPortionIndex);
Tag<RepeaterDayPortion<?>> t1Tag = t1.getTag(RepeaterDayPortion.class);
Object t1TagType = t1Tag.getType();
if (RepeaterDayPortion.DayPortion.MORNING.equals(t1TagType)) {
t1.untag(RepeaterDayPortion.class);
t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.AM));
} else if (RepeaterDayPortion.DayPortion.AFTERNOON.equals(t1TagType) ||
RepeaterDayPortion.DayPortion.EVENING.equals(t1TagType) ||
RepeaterDayPortion.DayPortion.NIGHT.equals(t1TagType)) {
t1.untag(RepeaterDayPortion.class);
t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.PM));
}
}
// handle ambiguous times if :ambiguous_time_range is specified
if (options.getAmbiguousTimeRange() != 0) {
List<Token> ttokens = new LinkedList<Token>();
for (int i = 0; i < tokenSize; i++) {
Token t0 = tokens.get(i);
ttokens.add(t0);
Token t1 = null;
if (i < tokenSize - 1) {
t1 = tokens.get(i + 1);
}
if (t0.getTag(RepeaterTime.class) != null &&
t0.getTag(RepeaterTime.class).getType().isAmbiguous() &&
(t1 == null || t1.getTag(RepeaterDayPortion.class) == null)) {
Token distoken = new Token("disambiguator");
distoken.tag(new IntegerRepeaterDayPortion(options.getAmbiguousTimeRange()));
ttokens.add(distoken);
}
}
tokens = ttokens;
}
return tokens;
}
public boolean isCompatible(Options options) {
return !options.isCompatibilityMode() || compatible;
}
public IHandler getHandler() {
return handler;
}
public boolean match(List<Token> tokens, Map<HandlerType, List<Handler>> definitions) {
int tokenIndex = 0;
for (HandlerPattern pattern : patterns) {
boolean optional = pattern.isOptional();
if (pattern instanceof TagPattern) {
boolean match = (tokenIndex < tokens.size() &&
tokens.get(tokenIndex).getTags(((TagPattern) pattern).getTagClass()).size() > 0);
if (!match && !optional) {
return false;
}
if (match) {
tokenIndex++;
}
// next if !match && optional ?
} else if (pattern instanceof HandlerTypePattern) {
if (optional && tokenIndex == tokens.size()) {
return true;
}
List<Handler> subHandlers = definitions.get(((HandlerTypePattern) pattern).getType());
for (Handler subHandler : subHandlers) {
if (subHandler.match(tokens.subList(tokenIndex, tokens.size()), definitions)) {
return true;
}
}
return false;
}
}
return tokenIndex == tokens.size();
}
@Override
public String toString() {
return "[Handler: " + handler + "]";
}
/**
*
*/
public enum HandlerType {
TIME, DATE, ANCHOR, ARROW, NARROW
}
}

View file

@ -0,0 +1,16 @@
package org.xbib.time.chronic.handlers;
/**
*
*/
public class HandlerPattern {
private boolean optional;
public HandlerPattern(boolean optional) {
this.optional = optional;
}
public boolean isOptional() {
return optional;
}
}

View file

@ -0,0 +1,21 @@
package org.xbib.time.chronic.handlers;
/**
*
*/
public class HandlerTypePattern extends HandlerPattern {
private Handler.HandlerType type;
public HandlerTypePattern(Handler.HandlerType type) {
this(type, false);
}
public HandlerTypePattern(Handler.HandlerType type, boolean optional) {
super(optional);
this.type = type;
}
public Handler.HandlerType getType() {
return type;
}
}

View file

@ -0,0 +1,14 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.List;
/**
*
*/
public interface IHandler {
Span handle(List<Token> tokens, Options options);
}

View file

@ -0,0 +1,25 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.Repeater;
import org.xbib.time.chronic.tags.Tag;
import java.time.ZonedDateTime;
import java.util.List;
/**
*
*/
public abstract class MDHandler implements IHandler {
public Span handle(Repeater<?> monthRep, Tag<Integer> day, List<Token> timeTokens, Options options) {
monthRep.setNow(options.getNow());
Span span = monthRep.thisSpan(options.getContext());
int year = span.getBeginCalendar().getYear();
int month = span.getBeginCalendar().getMonthValue();
ZonedDateTime dayStart = ZonedDateTime.of(year, month, day.getType(), 0, 0, 0, 0, options.getZoneId());
return Handler.dayOrTime(dayStart, timeTokens, options);
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.List;
/**
*
*/
public class ORGRHandler extends ORRHandler {
public Span handle(List<Token> tokens, Options options) {
Span outerSpan = Handler.getAnchor(tokens.subList(2, 4), options);
return handle(tokens.subList(0, 2), outerSpan, options);
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.Repeater;
import org.xbib.time.chronic.tags.Ordinal;
import org.xbib.time.chronic.tags.Pointer;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
*
*/
public abstract class ORRHandler implements IHandler {
public Span handle(List<Token> tokens, Span outerSpan, Options options) {
Repeater<?> repeater = tokens.get(1).getTag(Repeater.class);
repeater.setNow(outerSpan.getBeginCalendar().minus(1, ChronoUnit.SECONDS));
Integer ordinalValue = tokens.get(0).getTag(Ordinal.class).getType();
Span span = null;
for (int i = 0; i < ordinalValue; i++) {
span = repeater.nextSpan(Pointer.PointerType.FUTURE);
if (span.getBegin() > outerSpan.getEnd()) {
span = null;
break;
}
}
return span;
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.List;
/**
*
*/
public class ORSRHandler extends ORRHandler {
public Span handle(List<Token> tokens, Options options) {
Span outerSpan = Handler.getAnchor(tokens.subList(3, 4), options);
return handle(tokens.subList(0, 2), outerSpan, options);
}
}

View file

@ -0,0 +1,23 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class PSRHandler extends SRPHandler {
@Override
public Span handle(List<Token> tokens, Options options) {
List<Token> newTokens = new LinkedList<>();
newTokens.add(tokens.get(1));
newTokens.add(tokens.get(2));
newTokens.add(tokens.get(0));
return super.handle(newTokens, options);
}
}

View file

@ -0,0 +1,23 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class RGRHandler extends RHandler {
@Override
public Span handle(List<Token> tokens, Options options) {
List<Token> newTokens = new LinkedList<>();
newTokens.add(tokens.get(1));
newTokens.add(tokens.get(0));
newTokens.add(tokens.get(2));
return super.handle(newTokens, options);
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.List;
/**
*
*/
public class RHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
List<Token> ddTokens = Handler.dealiasAndDisambiguateTimes(tokens, options);
return Handler.getAnchor(ddTokens, options);
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.RepeaterMonthName;
import org.xbib.time.chronic.tags.ScalarDay;
import org.xbib.time.chronic.tags.ScalarYear;
import java.time.ZonedDateTime;
import java.util.List;
/**
*
*/
public class RdnRmnSdTTzSyHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
int month = tokens.get(1).getTag(RepeaterMonthName.class).getType().ordinal();
int day = tokens.get(2).getTag(ScalarDay.class).getType();
int year = tokens.get(5).getTag(ScalarYear.class).getType();
Span span;
try {
List<Token> timeTokens = tokens.subList(3, 4);
ZonedDateTime dayStart = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, options.getZoneId());
span = Handler.dayOrTime(dayStart, timeTokens, options);
} catch (IllegalArgumentException e) {
span = null;
}
return span;
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.RepeaterMonthName;
import org.xbib.time.chronic.tags.OrdinalDay;
import java.util.List;
/**
*
*/
public class RmnOdHandler extends MDHandler {
public Span handle(List<Token> tokens, Options options) {
return handle(tokens.get(0).getTag(RepeaterMonthName.class),
tokens.get(1).getTag(OrdinalDay.class), tokens.subList(2, tokens.size()), options);
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.RepeaterMonthName;
import org.xbib.time.chronic.tags.ScalarDay;
import java.util.List;
/**
*
*/
public class RmnSdHandler extends MDHandler {
public Span handle(List<Token> tokens, Options options) {
return handle(tokens.get(0).getTag(RepeaterMonthName.class),
tokens.get(1).getTag(ScalarDay.class), tokens.subList(2, tokens.size()), options);
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.RepeaterMonthName;
import org.xbib.time.chronic.tags.ScalarDay;
import org.xbib.time.chronic.tags.ScalarYear;
import java.time.ZonedDateTime;
import java.util.List;
/**
*
*/
public class RmnSdSyHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
int month = tokens.get(0).getTag(RepeaterMonthName.class).getType().ordinal();
int day = tokens.get(1).getTag(ScalarDay.class).getType();
int year = tokens.get(2).getTag(ScalarYear.class).getType();
Span span;
try {
List<Token> timeTokens = tokens.subList(3, tokens.size());
ZonedDateTime dayStart = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, options.getZoneId());
span = Handler.dayOrTime(dayStart, timeTokens, options);
} catch (IllegalArgumentException e) {
span = null;
}
return span;
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.RepeaterMonthName;
import org.xbib.time.chronic.tags.ScalarYear;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
*
*/
public class RmnSyHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
int month = tokens.get(0).getTag(RepeaterMonthName.class).getType().ordinal();
int year = tokens.get(1).getTag(ScalarYear.class).getType();
Span span;
try {
ZonedDateTime start = ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, options.getZoneId());
ZonedDateTime end = start.plus(1, ChronoUnit.MONTHS);
span = new Span(start, end);
} catch (IllegalArgumentException e) {
span = null;
}
return span;
}
}

View file

@ -0,0 +1,20 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.List;
/**
*
*/
public class SRPAHandler extends SRPHandler {
@Override
public Span handle(List<Token> tokens, Options options) {
Span anchorSpan = Handler.getAnchor(tokens.subList(3, tokens.size()), options);
return super.handle(tokens, anchorSpan, options);
}
}

View file

@ -0,0 +1,35 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Chronic;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.repeaters.Repeater;
import org.xbib.time.chronic.tags.Pointer;
import org.xbib.time.chronic.tags.Scalar;
import java.text.ParseException;
import java.util.List;
/**
*
*/
public class SRPHandler implements IHandler {
public Span handle(List<Token> tokens, Span span, Options options) {
int distance = tokens.get(0).getTag(Scalar.class).getType();
Repeater<?> repeater = tokens.get(1).getTag(Repeater.class);
Pointer.PointerType pointer = tokens.get(2).getTag(Pointer.class).getType();
return repeater.getOffset(span, distance, pointer);
}
@Override
public Span handle(List<Token> tokens, Options options) {
try {
Span span = Chronic.parse("this second", new Options().setNow(options.getNow()).setGuess(false));
return handle(tokens, span, options);
} catch (ParseException e) {
return null;
}
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class SdRmnSyHandler extends RmnSdSyHandler {
@Override
public Span handle(List<Token> tokens, Options options) {
List<Token> newTokens = new LinkedList<>();
newTokens.add(tokens.get(1));
newTokens.add(tokens.get(0));
newTokens.add(tokens.get(2));
newTokens.addAll(tokens.subList(3, tokens.size()));
return super.handle(newTokens, options);
}
}

View file

@ -0,0 +1,24 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class SdSmSyHandler extends SmSdSyHandler {
@Override
public Span handle(List<Token> tokens, Options options) {
List<Token> newTokens = new LinkedList<>();
newTokens.add(tokens.get(1));
newTokens.add(tokens.get(0));
newTokens.add(tokens.get(2));
newTokens.addAll(tokens.subList(3, tokens.size()));
return super.handle(newTokens, options);
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.ScalarDay;
import org.xbib.time.chronic.tags.ScalarMonth;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
*
*/
public class SmSdHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
int month = tokens.get(0).getTag(ScalarMonth.class).getType();
int day = tokens.get(1).getTag(ScalarDay.class).getType();
ZonedDateTime start = ZonedDateTime.of(options.getNow().getYear(), month, day, 0, 0, 0, 0, options.getZoneId());
ZonedDateTime end = start.plus(1, ChronoUnit.MONTHS);
return new Span(start, end);
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.ScalarDay;
import org.xbib.time.chronic.tags.ScalarMonth;
import org.xbib.time.chronic.tags.ScalarYear;
import java.time.ZonedDateTime;
import java.util.List;
/**
*
*/
public class SmSdSyHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
int month = tokens.get(0).getTag(ScalarMonth.class).getType();
int day = tokens.get(1).getTag(ScalarDay.class).getType();
int year = tokens.get(2).getTag(ScalarYear.class).getType();
Span span;
try {
List<Token> timeTokens = tokens.subList(3, tokens.size());
ZonedDateTime dayStart = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, options.getZoneId());
span = Handler.dayOrTime(dayStart, timeTokens, options);
} catch (IllegalArgumentException e) {
span = null;
}
return span;
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.ScalarMonth;
import org.xbib.time.chronic.tags.ScalarYear;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
*
*/
public class SmSyHandler implements IHandler {
public Span handle(List<Token> tokens, Options options) {
int month = tokens.get(0).getTag(ScalarMonth.class).getType();
int year = tokens.get(1).getTag(ScalarYear.class).getType();
Span span;
try {
ZonedDateTime start = ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, options.getZoneId());
ZonedDateTime end = start.plus(1, ChronoUnit.MONTHS);
span = new Span(start, end);
} catch (IllegalArgumentException e) {
span = null;
}
return span;
}
}

View file

@ -0,0 +1,24 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
public class SySmSdHandler extends SmSdSyHandler {
@Override
public Span handle(List<Token> tokens, Options options) {
List<Token> newTokens = new LinkedList<>();
newTokens.add(tokens.get(1));
newTokens.add(tokens.get(2));
newTokens.add(tokens.get(0));
newTokens.addAll(tokens.subList(3, tokens.size()));
return super.handle(newTokens, options);
}
}

View file

@ -0,0 +1,29 @@
package org.xbib.time.chronic.handlers;
import org.xbib.time.chronic.tags.Tag;
/**
*
*/
@SuppressWarnings("rawtypes")
public class TagPattern extends HandlerPattern {
private Class<? extends Tag> tagClass;
public TagPattern(Class<? extends Tag> tagClass) {
this(tagClass, false);
}
public TagPattern(Class<? extends Tag> tagClass, boolean optional) {
super(optional);
this.tagClass = tagClass;
}
public Class<? extends Tag> getTagClass() {
return tagClass;
}
@Override
public String toString() {
return "[TagPattern: tagClass = " + tagClass + "]";
}
}

View file

@ -0,0 +1,4 @@
/**
* Classes for chronic handlers.
*/
package org.xbib.time.chronic.handlers;

View file

@ -0,0 +1,194 @@
package org.xbib.time.chronic.numerizer;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*/
public class Numerizer {
private static final Pattern DEHYPHENATOR = Pattern.compile(" +|(\\D)-(\\D)");
private static final Pattern DEHALFER = Pattern.compile("a half", Pattern.CASE_INSENSITIVE);
private static final Pattern DEHAALFER = Pattern.compile("(\\d+)(?: | and |-)*haAlf", Pattern.CASE_INSENSITIVE);
private static final Pattern ANDITION_PATTERN = Pattern.compile("(\\d+)( | and )(\\d+)(?=\\W|$)");
private static final DirectNum[] DIRECT_NUMS;
private static final TenPrefix[] TEN_PREFIXES;
private static final BigPrefix[] BIG_PREFIXES;
static {
List<DirectNum> directNums = new LinkedList<>();
directNums.add(new DirectNum("eleven", "11"));
directNums.add(new DirectNum("twelve", "12"));
directNums.add(new DirectNum("thirteen", "13"));
directNums.add(new DirectNum("fourteen", "14"));
directNums.add(new DirectNum("fifteen", "15"));
directNums.add(new DirectNum("sixteen", "16"));
directNums.add(new DirectNum("seventeen", "17"));
directNums.add(new DirectNum("eighteen", "18"));
directNums.add(new DirectNum("nineteen", "19"));
directNums.add(new DirectNum("ninteen", "19"));
directNums.add(new DirectNum("zero", "0"));
directNums.add(new DirectNum("one", "1"));
directNums.add(new DirectNum("two", "2"));
directNums.add(new DirectNum("three", "3"));
directNums.add(new DirectNum("four(\\W|$)", "4$1"));
directNums.add(new DirectNum("five", "5"));
directNums.add(new DirectNum("six(\\W|$)", "6$1"));
directNums.add(new DirectNum("seven(\\W|$)", "7$1"));
directNums.add(new DirectNum("eight(\\W|$)", "8$1"));
directNums.add(new DirectNum("nine(\\W|$)", "9$1"));
directNums.add(new DirectNum("ten", "10"));
directNums.add(new DirectNum("\\ba\\b", "1"));
DIRECT_NUMS = directNums.toArray(new DirectNum[directNums.size()]);
List<TenPrefix> tenPrefixes = new LinkedList<>();
tenPrefixes.add(new TenPrefix("twenty", 20));
tenPrefixes.add(new TenPrefix("thirty", 30));
tenPrefixes.add(new TenPrefix("fourty", 40));
tenPrefixes.add(new TenPrefix("fifty", 50));
tenPrefixes.add(new TenPrefix("sixty", 60));
tenPrefixes.add(new TenPrefix("seventy", 70));
tenPrefixes.add(new TenPrefix("eighty", 80));
tenPrefixes.add(new TenPrefix("ninety", 90));
tenPrefixes.add(new TenPrefix("ninty", 90));
TEN_PREFIXES = tenPrefixes.toArray(new TenPrefix[tenPrefixes.size()]);
List<BigPrefix> bigPrefixes = new LinkedList<>();
bigPrefixes.add(new BigPrefix("hundred", 100L));
bigPrefixes.add(new BigPrefix("thousand", 1000L));
bigPrefixes.add(new BigPrefix("million", 1000000L));
bigPrefixes.add(new BigPrefix("billion", 1000000000L));
bigPrefixes.add(new BigPrefix("trillion", 1000000000000L));
BIG_PREFIXES = bigPrefixes.toArray(new BigPrefix[bigPrefixes.size()]);
}
public static String numerize(String str) {
String numerizedStr = str;
numerizedStr = Numerizer.DEHYPHENATOR.matcher(numerizedStr).replaceAll("$1 $2");
numerizedStr = Numerizer.DEHALFER.matcher(numerizedStr).replaceAll("haAlf");
for (DirectNum dn : Numerizer.DIRECT_NUMS) {
numerizedStr = dn.getName().matcher(numerizedStr).replaceAll(dn.getNumber());
}
for (Prefix tp : Numerizer.TEN_PREFIXES) {
Matcher matcher = tp.getName().matcher(numerizedStr);
if (matcher.find()) {
StringBuffer matcherBuffer = new StringBuffer();
do {
if (matcher.group(1) == null) {
matcher.appendReplacement(matcherBuffer, String.valueOf(tp.getNumber()));
} else {
matcher.appendReplacement(matcherBuffer, String.valueOf(tp.getNumber() +
Long.parseLong(matcher.group(1).trim())));
}
} while (matcher.find());
matcher.appendTail(matcherBuffer);
numerizedStr = matcherBuffer.toString();
}
}
for (Prefix bp : Numerizer.BIG_PREFIXES) {
Matcher matcher = bp.getName().matcher(numerizedStr);
if (matcher.find()) {
StringBuffer matcherBuffer = new StringBuffer();
do {
if (matcher.group(1) == null) {
matcher.appendReplacement(matcherBuffer, String.valueOf(bp.getNumber()));
} else {
matcher.appendReplacement(matcherBuffer, String.valueOf(bp.getNumber() *
Long.parseLong(matcher.group(1).trim())));
}
} while (matcher.find());
matcher.appendTail(matcherBuffer);
numerizedStr = matcherBuffer.toString();
numerizedStr = Numerizer.andition(numerizedStr);
}
}
Matcher matcher = Numerizer.DEHAALFER.matcher(numerizedStr);
if (matcher.find()) {
StringBuffer matcherBuffer = new StringBuffer();
do {
matcher.appendReplacement(matcherBuffer,
String.valueOf(Float.parseFloat(matcher.group(1).trim()) + 0.5f));
} while (matcher.find());
matcher.appendTail(matcherBuffer);
numerizedStr = matcherBuffer.toString();
}
return numerizedStr;
}
private static String andition(String str) {
StringBuffer anditionStr = new StringBuffer(str);
Matcher matcher = Numerizer.ANDITION_PATTERN.matcher(anditionStr);
while (matcher.find()) {
if (matcher.group(2).equalsIgnoreCase(" and ") || matcher.group(1).length() > matcher.group(3).length()) {
anditionStr.replace(matcher.start(), matcher.end(),
String.valueOf(Integer.parseInt(matcher.group(1).trim()) +
Integer.parseInt(matcher.group(3).trim())));
matcher = Numerizer.ANDITION_PATTERN.matcher(anditionStr);
}
}
return anditionStr.toString();
}
/**
*
*/
static class DirectNum {
private Pattern name;
private String number;
DirectNum(String name, String number) {
this.name = Pattern.compile(name, Pattern.CASE_INSENSITIVE);
this.number = number;
}
public Pattern getName() {
return name;
}
public String getNumber() {
return number;
}
}
/**
*
*/
static class Prefix {
private Pattern name;
private long number;
Prefix(Pattern name, long number) {
this.name = name;
this.number = number;
}
public Pattern getName() {
return name;
}
public long getNumber() {
return number;
}
}
private static class TenPrefix extends Prefix {
TenPrefix(String name, long number) {
super(Pattern.compile("(?:" + name + ")( *\\d(?=\\D|$))*", Pattern.CASE_INSENSITIVE), number);
}
}
private static class BigPrefix extends Prefix {
BigPrefix(String name, long number) {
super(Pattern.compile("(\\d*) *" + name, Pattern.CASE_INSENSITIVE), number);
}
}
}

View file

@ -0,0 +1,4 @@
/**
* Classes for chronic numerizers.
*/
package org.xbib.time.chronic.numerizer;

View file

@ -0,0 +1,4 @@
/**
* Classes for time chronic.
*/
package org.xbib.time.chronic;

View file

@ -0,0 +1,45 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Range;
/**
*
*/
public class EnumRepeaterDayPortion extends RepeaterDayPortion<RepeaterDayPortion.DayPortion> {
private static final Range AM_RANGE = new Range(0, 12 * 60 * 60); // 12am-12pm
private static final Range PM_RANGE = new Range(12 * 60 * 60, 24 * 60 * 60 - 1); // 12pm-12am
private static final Range MORNING_RANGE = new Range(6 * 60 * 60, 12 * 60 * 60); // 6am-12pm
private static final Range AFTERNOON_RANGE = new Range(13 * 60 * 60, 17 * 60 * 60); // 1pm-5pm
private static final Range EVENING_RANGE = new Range(17 * 60 * 60, 20 * 60 * 60); // 5pm-8pm
private static final Range NIGHT_RANGE = new Range(20 * 60 * 60, 24 * 60 * 60); // 8pm-12pm
public EnumRepeaterDayPortion(DayPortion type) {
super(type);
}
@Override
protected Range createRange(DayPortion type) {
Range range;
if (type == DayPortion.AM) {
range = EnumRepeaterDayPortion.AM_RANGE;
} else if (type == DayPortion.PM) {
range = EnumRepeaterDayPortion.PM_RANGE;
} else if (type == DayPortion.MORNING) {
range = EnumRepeaterDayPortion.MORNING_RANGE;
} else if (type == DayPortion.AFTERNOON) {
range = EnumRepeaterDayPortion.AFTERNOON_RANGE;
} else if (type == DayPortion.EVENING) {
range = EnumRepeaterDayPortion.EVENING_RANGE;
} else if (type == DayPortion.NIGHT) {
range = EnumRepeaterDayPortion.NIGHT_RANGE;
} else {
throw new IllegalArgumentException("Unknown day portion type " + type);
}
return range;
}
@Override
protected long getWidth(Range range) {
return range.getWidth();
}
}

View file

@ -0,0 +1,22 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Range;
/**
*
*/
public class IntegerRepeaterDayPortion extends RepeaterDayPortion<Integer> {
public IntegerRepeaterDayPortion(Integer type) {
super(type);
}
@Override
protected Range createRange(Integer type) {
return new Range(type * 60L * 60L, (type + 12) * 60L * 60L);
}
@Override
protected long getWidth(Range range) {
return 12 * 60L * 60L;
}
}

View file

@ -0,0 +1,91 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.Pointer;
import org.xbib.time.chronic.tags.Tag;
import java.util.List;
/**
* Repeater.
* @param <T> type parameter
*/
public abstract class Repeater<T> extends Tag<T> implements Comparable<Repeater<?>> {
public Repeater(T type) {
super(type);
}
public static List<Token> scan(List<Token> tokens) {
return Repeater.scan(tokens, new Options());
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (Token token : tokens) {
Tag<?> t;
t = RepeaterMonthName.scan(token);
if (t != null) {
token.tag(t);
}
t = RepeaterDayName.scan(token);
if (t != null) {
token.tag(t);
}
t = RepeaterDayPortion.scan(token);
if (t != null) {
token.tag(t);
}
t = RepeaterTime.scan(token, tokens, options);
if (t != null) {
token.tag(t);
}
t = RepeaterUnit.scan(token);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
public int compareTo(Repeater<?> other) {
return Integer.compare(getWidth(), other.getWidth());
}
/**
* Returns the width (in seconds or months) of this repeatable.
* @return width
*/
public abstract int getWidth();
/**
* Returns the next occurance of this repeatable.
* @param pointer pointer
* @return span
*/
public Span nextSpan(Pointer.PointerType pointer) {
if (getNow() == null) {
throw new IllegalStateException("Start point must be set before calling #next");
}
return internalNextSpan(pointer);
}
protected abstract Span internalNextSpan(Pointer.PointerType pointer);
public Span thisSpan(Pointer.PointerType pointer) {
if (getNow() == null) {
throw new IllegalStateException("Start point must be set before calling #this");
}
return internalThisSpan(pointer);
}
protected abstract Span internalThisSpan(Pointer.PointerType pointer);
public abstract Span getOffset(Span span, int amount, Pointer.PointerType pointer);
@Override
public String toString() {
return "repeater";
}
}

View file

@ -0,0 +1,72 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterDay extends RepeaterUnit {
public static final int DAY_SECONDS = 86400;
private ZonedDateTime currentDayStart;
private static ZonedDateTime ymd(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
0, 0, 0, 0, zonedDateTime.getZone());
}
private static ZonedDateTime ymdh(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
zonedDateTime.getHour(), 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentDayStart == null) {
currentDayStart = ymd(getNow());
}
int direction = pointer == PointerType.FUTURE ? 1 : -1;
currentDayStart = currentDayStart.plus(direction, ChronoUnit.DAYS);
return new Span(currentDayStart, ChronoUnit.DAYS, 1);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
ZonedDateTime dayBegin;
ZonedDateTime dayEnd;
if (pointer == PointerType.FUTURE) {
dayBegin = ymdh(getNow()).plus(1, ChronoUnit.HOURS);
dayEnd = ymd(getNow()).plus(1, ChronoUnit.DAYS);
} else if (pointer == PointerType.PAST) {
dayBegin = ymd(getNow());
dayEnd = ymdh(getNow());
} else if (pointer == PointerType.NONE) {
dayBegin = ymd(getNow());
dayEnd = ymdh(getNow()).plus(1, ChronoUnit.DAYS);
} else {
throw new IllegalArgumentException("unable to handle pointer " + pointer + ".");
}
return new Span(dayBegin, dayEnd);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
return span.add(direction * amount * RepeaterDay.DAY_SECONDS);
}
@Override
public int getWidth() {
return RepeaterDay.DAY_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-day";
}
}

View file

@ -0,0 +1,106 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class RepeaterDayName extends Repeater<RepeaterDayName.DayName> {
public static final int DAY_SECONDS = 86400; // (24 * 60 * 60);
private static final Pattern MON_PATTERN = Pattern.compile("^m[ou]n(day)?$");
private static final Pattern TUE_PATTERN = Pattern.compile("^t(ue|eu|oo|u|)s(day)?$");
private static final Pattern TUE_PATTERN_1 = Pattern.compile("^tue$");
private static final Pattern WED_PATTERN_1 = Pattern.compile("^we(dnes|nds|nns)day$");
private static final Pattern WED_PATTERN_2 = Pattern.compile("^wed$");
private static final Pattern THU_PATTERN_1 = Pattern.compile("^th(urs|ers)day$");
private static final Pattern THU_PATTERN_2 = Pattern.compile("^thu$");
private static final Pattern FRI_PATTERN = Pattern.compile("^fr[iy](day)?$");
private static final Pattern SAT_PATTERN = Pattern.compile("^sat(t?[ue]rday)?$");
private static final Pattern SUN_PATTERN = Pattern.compile("^su[nm](day)?$");
private ZonedDateTime currentDayStart;
public RepeaterDayName(DayName type) {
super(type);
}
public static RepeaterDayName scan(Token token) {
Map<Pattern, DayName> scanner = new HashMap<>();
scanner.put(RepeaterDayName.MON_PATTERN, DayName.MONDAY);
scanner.put(RepeaterDayName.TUE_PATTERN, DayName.TUESDAY);
scanner.put(RepeaterDayName.TUE_PATTERN_1, DayName.TUESDAY);
scanner.put(RepeaterDayName.WED_PATTERN_1, DayName.WEDNESDAY);
scanner.put(RepeaterDayName.WED_PATTERN_2, DayName.WEDNESDAY);
scanner.put(RepeaterDayName.THU_PATTERN_1, DayName.THURSDAY);
scanner.put(RepeaterDayName.THU_PATTERN_2, DayName.THURSDAY);
scanner.put(RepeaterDayName.FRI_PATTERN, DayName.FRIDAY);
scanner.put(RepeaterDayName.SAT_PATTERN, DayName.SATURDAY);
scanner.put(RepeaterDayName.SUN_PATTERN, DayName.SUNDAY);
for (Map.Entry<Pattern, DayName> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new RepeaterDayName(scanner.get(scannerItem));
}
}
return null;
}
private static ZonedDateTime ymd(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
0, 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
int direction = (pointer == PointerType.FUTURE) ? 1 : -1;
if (currentDayStart == null) {
currentDayStart = ymd(getNow());
currentDayStart = currentDayStart.plus(direction, ChronoUnit.DAYS);
int dayNum = getType().ordinal();
while ((currentDayStart.get(ChronoField.DAY_OF_WEEK) - 1) != dayNum) {
currentDayStart = currentDayStart.plus(direction, ChronoUnit.DAYS);
}
} else {
currentDayStart = currentDayStart.plus(direction * 7, ChronoUnit.DAYS);
}
return new Span(currentDayStart, ChronoUnit.DAYS, 1);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
if (pointer == PointerType.NONE) {
pointer = PointerType.FUTURE;
}
return super.nextSpan(pointer);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
throw new IllegalStateException("Not implemented.");
}
@Override
public int getWidth() {
return RepeaterDayName.DAY_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-dayname-" + getType();
}
/**
*
*/
public enum DayName {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
}

View file

@ -0,0 +1,147 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Range;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Repeater day portion.
* @param <T> type parameter
*/
public abstract class RepeaterDayPortion<T> extends Repeater<T> {
private static final Pattern AM_PATTERN = Pattern.compile("^ams?$");
private static final Pattern PM_PATTERN = Pattern.compile("^pms?$");
private static final Pattern MORNING_PATTERN = Pattern.compile("^mornings?$");
private static final Pattern AFTERNOON_PATTERN = Pattern.compile("^afternoons?$");
private static final Pattern EVENING_PATTERN = Pattern.compile("^evenings?$");
private static final Pattern NIGHT_PATTERN = Pattern.compile("^(night|nite)s?$");
private static final int FULL_DAY_SECONDS = 60 * 60 * 24;
private Range range;
private Span currentSpan;
public RepeaterDayPortion(T type) {
super(type);
range = createRange(type);
}
public static EnumRepeaterDayPortion scan(Token token) {
Map<Pattern, DayPortion> scanner = new HashMap<>();
scanner.put(RepeaterDayPortion.AM_PATTERN, DayPortion.AM);
scanner.put(RepeaterDayPortion.PM_PATTERN, DayPortion.PM);
scanner.put(RepeaterDayPortion.MORNING_PATTERN, DayPortion.MORNING);
scanner.put(RepeaterDayPortion.AFTERNOON_PATTERN, DayPortion.AFTERNOON);
scanner.put(RepeaterDayPortion.EVENING_PATTERN, DayPortion.EVENING);
scanner.put(RepeaterDayPortion.NIGHT_PATTERN, DayPortion.NIGHT);
for (Map.Entry<Pattern, DayPortion> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new EnumRepeaterDayPortion(scanner.get(scannerItem));
}
}
return null;
}
private static ZonedDateTime ymd(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
0, 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
ZonedDateTime rangeStart;
ZonedDateTime rangeEnd;
if (currentSpan == null) {
long nowSeconds = getNow().toInstant().getEpochSecond() - ymd(getNow()).toInstant().getEpochSecond();
if (nowSeconds < range.getBegin()) {
if (pointer == PointerType.FUTURE) {
rangeStart = ymd(getNow()).plus(range.getBegin(), ChronoUnit.SECONDS);
} else if (pointer == PointerType.PAST) {
rangeStart = ymd(getNow()).minus(1, ChronoUnit.DAYS).plus(range.getBegin(), ChronoUnit.SECONDS);
} else {
throw new IllegalArgumentException("unable to handle pointer type " + pointer);
}
} else if (nowSeconds > range.getBegin()) {
if (pointer == PointerType.FUTURE) {
rangeStart = ymd(getNow()).plus(1, ChronoUnit.DAYS).plus(range.getBegin(), ChronoUnit.SECONDS);
} else if (pointer == PointerType.PAST) {
rangeStart = ymd(getNow()).plus(range.getBegin(), ChronoUnit.SECONDS);
} else {
throw new IllegalArgumentException("unable to handle pointer type " + pointer);
}
} else {
if (pointer == PointerType.FUTURE) {
rangeStart = ymd(getNow()).plus(1, ChronoUnit.DAYS).plus(range.getBegin(), ChronoUnit.SECONDS);
} else if (pointer == PointerType.PAST) {
rangeStart = ymd(getNow()).minus(1, ChronoUnit.DAYS).plus(range.getBegin(), ChronoUnit.SECONDS);
} else {
throw new IllegalArgumentException("unable to handle pointer type " + pointer);
}
}
currentSpan = new Span(rangeStart, rangeStart.plus(range.getWidth(), ChronoUnit.SECONDS));
} else {
if (pointer == PointerType.FUTURE) {
currentSpan = currentSpan.add(RepeaterDayPortion.FULL_DAY_SECONDS);
} else if (pointer == PointerType.PAST) {
currentSpan = currentSpan.add(-RepeaterDayPortion.FULL_DAY_SECONDS);
} else {
throw new IllegalArgumentException("Unable to handle pointer type " + pointer);
}
}
return currentSpan;
}
@Override
protected Span internalThisSpan(PointerType pointer) {
ZonedDateTime rangeStart = ymd(getNow()).plus(range.getBegin(), ChronoUnit.SECONDS);
currentSpan = new Span(rangeStart, rangeStart.plus(range.getWidth(), ChronoUnit.SECONDS));
return currentSpan;
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
setNow(span.getBeginCalendar());
Span portionSpan = nextSpan(pointer);
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
portionSpan = portionSpan.add(direction * (amount - 1) * RepeaterDay.DAY_SECONDS);
return portionSpan;
}
@Override
public int getWidth() {
if (range == null) {
throw new IllegalStateException("Range has not been set");
}
Long width;
if (currentSpan != null) {
width = currentSpan.getWidth();
} else {
width = getWidth(range);
}
return width.intValue();
}
protected abstract long getWidth(Range range);
protected abstract Range createRange(T type);
@Override
public String toString() {
return super.toString() + "-dayportion-" + getType();
}
/**
*
*/
public enum DayPortion {
AM, PM, MORNING, AFTERNOON, EVENING, NIGHT
}
}

View file

@ -0,0 +1,93 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterFortnight extends RepeaterUnit {
public static final int FORTNIGHT_SECONDS = 1209600; // (14 * 24 * 60 * 60)
private ZonedDateTime currentFortnightStart;
private static ZonedDateTime ymdh(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
zonedDateTime.getHour(), 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentFortnightStart == null) {
if (pointer == PointerType.FUTURE) {
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
Span nextSundaySpan = sundayRepeater.nextSpan(PointerType.FUTURE);
currentFortnightStart = nextSundaySpan.getBeginCalendar();
} else if (pointer == PointerType.PAST) {
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow().plus(RepeaterDay.DAY_SECONDS, ChronoUnit.SECONDS));
sundayRepeater.nextSpan(PointerType.PAST);
sundayRepeater.nextSpan(PointerType.PAST);
Span lastSundaySpan = sundayRepeater.nextSpan(PointerType.PAST);
currentFortnightStart = lastSundaySpan.getBeginCalendar();
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
} else {
long direction = (pointer == PointerType.FUTURE) ? 1L : -1L;
currentFortnightStart = currentFortnightStart.plus(direction * RepeaterFortnight.FORTNIGHT_SECONDS,
ChronoUnit.SECONDS);
}
return new Span(currentFortnightStart, ChronoUnit.SECONDS, RepeaterFortnight.FORTNIGHT_SECONDS);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
if (pointer == null) {
pointer = PointerType.FUTURE;
}
Span span;
if (pointer == PointerType.FUTURE) {
ZonedDateTime thisFortnightStart = ymdh(getNow()).plus(RepeaterHour.HOUR_SECONDS, ChronoUnit.SECONDS);
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
sundayRepeater.thisSpan(PointerType.FUTURE);
Span thisSundaySpan = sundayRepeater.thisSpan(PointerType.FUTURE);
ZonedDateTime thisFortnightEnd = thisSundaySpan.getBeginCalendar();
span = new Span(thisFortnightStart, thisFortnightEnd);
} else if (pointer == PointerType.PAST) {
ZonedDateTime thisFortnightEnd = ymdh(getNow());
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
Span lastSundaySpan = sundayRepeater.nextSpan(PointerType.PAST);
ZonedDateTime thisFortnightStart = lastSundaySpan.getBeginCalendar();
span = new Span(thisFortnightStart, thisFortnightEnd);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return span;
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = (pointer == PointerType.FUTURE) ? 1L : -1L;
return span.add(direction * amount * RepeaterFortnight.FORTNIGHT_SECONDS);
}
@Override
public int getWidth() {
return RepeaterFortnight.FORTNIGHT_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-fortnight";
}
}

View file

@ -0,0 +1,78 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterHour extends RepeaterUnit {
public static final int HOUR_SECONDS = 3600;
private ZonedDateTime currentDayStart;
private static ZonedDateTime ymdh(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
zonedDateTime.getHour(), 0, 0, 0, zonedDateTime.getZone());
}
private static ZonedDateTime ymdhm(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
zonedDateTime.getHour(), zonedDateTime.getMinute(), 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentDayStart == null) {
if (pointer == PointerType.FUTURE) {
currentDayStart = ymdh(getNow()).plus(1, ChronoUnit.HOURS);
} else if (pointer == PointerType.PAST) {
currentDayStart = ymdh(getNow()).minus(1, ChronoUnit.HOURS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
} else {
int direction = (pointer == PointerType.FUTURE) ? 1 : -1;
currentDayStart = currentDayStart.plus(direction, ChronoUnit.HOURS);
}
return new Span(currentDayStart, ChronoUnit.HOURS, 1);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
ZonedDateTime hourStart;
ZonedDateTime hourEnd;
if (pointer == PointerType.FUTURE) {
hourStart = ymdhm(getNow()).plus(1, ChronoUnit.MINUTES);
hourEnd = ymdh(getNow()).plus(1, ChronoUnit.HOURS);
} else if (pointer == PointerType.PAST) {
hourStart = ymdh(getNow());
hourEnd = ymdhm(getNow());
} else if (pointer == PointerType.NONE) {
hourStart = ymdh(getNow());
hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return new Span(hourStart, hourEnd);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
return span.add(direction * amount * RepeaterHour.HOUR_SECONDS);
}
@Override
public int getWidth() {
return RepeaterHour.HOUR_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-hour";
}
}

View file

@ -0,0 +1,73 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterMinute extends RepeaterUnit {
public static final int MINUTE_SECONDS = 60;
private ZonedDateTime currentMinuteStart;
private static ZonedDateTime ymdhm(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
zonedDateTime.getHour(), zonedDateTime.getMinute(), 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentMinuteStart == null) {
if (pointer == PointerType.FUTURE) {
currentMinuteStart = ymdhm(getNow()).plus(1, ChronoUnit.MINUTES);
} else if (pointer == PointerType.PAST) {
currentMinuteStart = ymdhm(getNow()).minus(1, ChronoUnit.MINUTES);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
} else {
int direction = pointer == PointerType.FUTURE ? 1 : -1;
currentMinuteStart = currentMinuteStart.plus(direction, ChronoUnit.MINUTES);
}
return new Span(currentMinuteStart, ChronoUnit.SECONDS, RepeaterMinute.MINUTE_SECONDS);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
ZonedDateTime minuteBegin;
ZonedDateTime minuteEnd;
if (pointer == PointerType.FUTURE) {
minuteBegin = getNow();
minuteEnd = ymdhm(getNow());
} else if (pointer == PointerType.PAST) {
minuteBegin = ymdhm(getNow());
minuteEnd = getNow();
} else if (pointer == PointerType.NONE) {
minuteBegin = ymdhm(getNow());
minuteEnd = ymdhm(getNow()).plus(RepeaterMinute.MINUTE_SECONDS, ChronoUnit.SECONDS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return new Span(minuteBegin, minuteEnd);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
return span.add(direction * amount * RepeaterMinute.MINUTE_SECONDS);
}
@Override
public int getWidth() {
return RepeaterMinute.MINUTE_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-minute";
}
}

View file

@ -0,0 +1,73 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterMonth extends RepeaterUnit {
private static final int MONTH_SECONDS = 2592000; // 30 * 24 * 60 * 60
private ZonedDateTime currentMonthStart;
private static ZonedDateTime ymd(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
0, 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
int direction = (pointer == PointerType.FUTURE) ? 1 : -1;
if (currentMonthStart == null) {
currentMonthStart = ZonedDateTime.of(getNow().getYear(), getNow().getMonthValue(), 1, 0, 0, 0, 0,
getNow().getZone())
.plus(direction, ChronoUnit.MONTHS);
} else {
currentMonthStart = currentMonthStart.plus(direction, ChronoUnit.MONTHS);
}
return new Span(currentMonthStart, ChronoUnit.MONTHS, 1);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long l = amount * (pointer == PointerType.FUTURE ? 1L : -1L);
return new Span(span.getBeginCalendar().plus(l, ChronoUnit.MONTHS), span.getEndCalendar().plus(l,
ChronoUnit.MONTHS));
}
@Override
protected Span internalThisSpan(PointerType pointer) {
ZonedDateTime monthStart;
ZonedDateTime monthEnd;
if (pointer == PointerType.FUTURE) {
monthStart = ymd(getNow()).plus(1, ChronoUnit.DAYS);
monthEnd = ZonedDateTime.of(getNow().getYear(), getNow().getMonthValue(), 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.MONTHS);
} else if (pointer == PointerType.PAST) {
monthStart = ZonedDateTime.of(getNow().getYear(), getNow().getMonthValue(), 1, 0, 0, 0, 0, getNow().getZone());
monthEnd = ymd(getNow());
} else if (pointer == PointerType.NONE) {
monthStart = ZonedDateTime.of(getNow().getYear(), getNow().getMonthValue(), 1, 0, 0, 0, 0, getNow().getZone());
monthEnd = ZonedDateTime.of(getNow().getYear(), getNow().getMonthValue(), 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.MONTHS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return new Span(monthStart, monthEnd);
}
@Override
public int getWidth() {
return RepeaterMonth.MONTH_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-month";
}
}

View file

@ -0,0 +1,145 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class RepeaterMonthName extends Repeater<RepeaterMonthName.MonthName> {
private static final Pattern JAN_PATTERN = Pattern.compile("^jan\\.?(uary)?$");
private static final Pattern FEB_PATTERN = Pattern.compile("^feb\\.?(ruary)?$");
private static final Pattern MAR_PATTERN = Pattern.compile("^mar\\.?(ch)?$");
private static final Pattern APR_PATTERN = Pattern.compile("^apr\\.?(il)?$");
private static final Pattern MAY_PATTERN = Pattern.compile("^may$");
private static final Pattern JUN_PATTERN = Pattern.compile("^jun\\.?e?$");
private static final Pattern JUL_PATTERN = Pattern.compile("^jul\\.?y?$");
private static final Pattern AUG_PATTERN = Pattern.compile("^aug\\.?(ust)?$");
private static final Pattern SEP_PATTERN = Pattern.compile("^sep\\.?(t\\.?|tember)?$");
private static final Pattern OCT_PATTERN = Pattern.compile("^oct\\.?(ober)?$");
private static final Pattern NOV_PATTERN = Pattern.compile("^nov\\.?(ember)?$");
private static final Pattern DEC_PATTERN = Pattern.compile("^dec\\.?(ember)?$");
private static final int MONTH_SECONDS = 2592000; // 30 * 24 * 60 * 60
private ZonedDateTime currentMonthBegin;
public RepeaterMonthName(MonthName type) {
super(type);
}
public static RepeaterMonthName scan(Token token) {
Map<Pattern, MonthName> scanner = new HashMap<>();
scanner.put(RepeaterMonthName.JAN_PATTERN, MonthName.JANUARY);
scanner.put(RepeaterMonthName.FEB_PATTERN, MonthName.FEBRUARY);
scanner.put(RepeaterMonthName.MAR_PATTERN, MonthName.MARCH);
scanner.put(RepeaterMonthName.APR_PATTERN, MonthName.APRIL);
scanner.put(RepeaterMonthName.MAY_PATTERN, MonthName.MAY);
scanner.put(RepeaterMonthName.JUN_PATTERN, MonthName.JUNE);
scanner.put(RepeaterMonthName.JUL_PATTERN, MonthName.JULY);
scanner.put(RepeaterMonthName.AUG_PATTERN, MonthName.AUGUST);
scanner.put(RepeaterMonthName.SEP_PATTERN, MonthName.SEPTEMBER);
scanner.put(RepeaterMonthName.OCT_PATTERN, MonthName.OCTOBER);
scanner.put(RepeaterMonthName.NOV_PATTERN, MonthName.NOVEMBER);
scanner.put(RepeaterMonthName.DEC_PATTERN, MonthName.DECEMBER);
for (Map.Entry<Pattern, MonthName> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new RepeaterMonthName(scanner.get(scannerItem));
}
}
return null;
}
public int getIndex() {
return getType().ordinal();
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentMonthBegin == null) {
int targetMonth = getType().ordinal();
int nowMonth = getNow().getMonthValue();
if (pointer == PointerType.FUTURE) {
if (nowMonth < targetMonth) {
currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone());
} else if (nowMonth > targetMonth) {
currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.YEARS);
}
} else if (pointer == PointerType.NONE) {
if (nowMonth <= targetMonth) {
currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone());
} else if (nowMonth > targetMonth) {
currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.YEARS);
}
} else if (pointer == PointerType.PAST) {
if (nowMonth > targetMonth) {
currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone());
} else if (nowMonth <= targetMonth) {
currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone())
.minus(1, ChronoUnit.YEARS);
}
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
if (currentMonthBegin == null) {
throw new IllegalStateException("Current month should be set by now.");
}
} else {
if (pointer == PointerType.FUTURE) {
currentMonthBegin = currentMonthBegin.plus(1, ChronoUnit.YEARS);
} else if (pointer == PointerType.PAST) {
currentMonthBegin = currentMonthBegin.minus(1, ChronoUnit.YEARS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
}
return new Span(currentMonthBegin, ChronoUnit.MONTHS, 1);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
Span span;
if (pointer == PointerType.PAST) {
span = nextSpan(pointer);
} else if (pointer == PointerType.FUTURE || pointer == PointerType.NONE) {
span = nextSpan(PointerType.NONE);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return span;
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
throw new IllegalStateException("Not implemented.");
}
@Override
public int getWidth() {
return RepeaterMonthName.MONTH_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-monthname-" + getType();
}
/**
*
*/
public enum MonthName {
ZEROMONTH_DO_NOT_REMOVE,
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER
}
}

View file

@ -0,0 +1,49 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterSecond extends RepeaterUnit {
public static final int SECOND_SECONDS = 1; // (60 * 60);
private ZonedDateTime secondStart;
@Override
protected Span internalNextSpan(PointerType pointer) {
int direction = pointer == PointerType.FUTURE ? 1 : -1;
if (secondStart == null) {
secondStart = getNow().plus(direction, ChronoUnit.SECONDS);
} else {
secondStart = secondStart.plus(direction, ChronoUnit.SECONDS);
}
return new Span(secondStart, ChronoUnit.SECONDS, 1);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
return new Span(getNow(), ChronoUnit.SECONDS, 1);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
return span.add(direction * amount * RepeaterSecond.SECOND_SECONDS);
}
@Override
public int getWidth() {
return RepeaterSecond.SECOND_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-second";
}
}

View file

@ -0,0 +1,223 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.Tick;
import org.xbib.time.chronic.Token;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
/**
*
*/
public class RepeaterTime extends Repeater<Tick> {
private static final Pattern TIME_PATTERN = Pattern.compile("^\\d{1,2}(:?\\d{2})?([\\.:]?\\d{2})?$");
private ZonedDateTime currentTime;
public RepeaterTime(String time) {
super(null);
String t = time.replaceAll(":", "");
Tick type;
int length = t.length();
if (length <= 2) {
int hours = Integer.parseInt(t);
int hoursInSeconds = hours * 60 * 60;
if (hours == 12) {
type = new Tick(0, true);
} else {
type = new Tick(hoursInSeconds, true);
}
} else if (length == 3) {
int hoursInSeconds = Integer.parseInt(t.substring(0, 1)) * 60 * 60;
int minutesInSeconds = Integer.parseInt(t.substring(1)) * 60;
type = new Tick(hoursInSeconds + minutesInSeconds, true);
} else if (length == 4) {
boolean ambiguous = (time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 &&
Integer.parseInt(t.substring(0, 2)) <= 12);
int hours = Integer.parseInt(t.substring(0, 2));
int hoursInSeconds = hours * 60 * 60;
int minutesInSeconds = Integer.parseInt(t.substring(2)) * 60;
if (hours == 12) {
type = new Tick(minutesInSeconds, ambiguous);
} else {
type = new Tick(hoursInSeconds + minutesInSeconds, ambiguous);
}
} else if (length == 5) {
int hoursInSeconds = Integer.parseInt(t.substring(0, 1)) * 60 * 60;
int minutesInSeconds = Integer.parseInt(t.substring(1, 3)) * 60;
int seconds = Integer.parseInt(t.substring(3));
type = new Tick(hoursInSeconds + minutesInSeconds + seconds, true);
} else if (length == 6) {
boolean ambiguous = (time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 &&
Integer.parseInt(t.substring(0, 2)) <= 12);
int hours = Integer.parseInt(t.substring(0, 2));
int hoursInSeconds = hours * 60 * 60;
int minutesInSeconds = Integer.parseInt(t.substring(2, 4)) * 60;
int seconds = Integer.parseInt(t.substring(4, 6));
if (hours == 12) {
type = new Tick(minutesInSeconds + seconds, ambiguous);
} else {
type = new Tick(hoursInSeconds + minutesInSeconds + seconds, ambiguous);
}
} else {
throw new IllegalArgumentException("Time cannot exceed six digits");
}
setType(type);
}
public static RepeaterTime scan(Token token, List<Token> tokens, Options options) {
if (RepeaterTime.TIME_PATTERN.matcher(token.getWord()).matches()) {
return new RepeaterTime(token.getWord());
}
Integer intStrValue = integerValue(token.getWord());
if (intStrValue != null) {
return new RepeaterTime(intStrValue.toString());
}
return null;
}
private static Integer integerValue(String str) {
if (str != null) {
String s = str.toLowerCase();
if ("one".equals(s)) {
return 1;
} else if ("two".equals(s)) {
return 2;
} else if ("three".equals(s)) {
return 3;
} else if ("four".equals(s)) {
return 4;
} else if ("five".equals(s)) {
return 5;
} else if ("six".equals(s)) {
return 6;
} else if ("seven".equals(s)) {
return 7;
} else if ("eight".equals(s)) {
return 8;
} else if ("nine".equals(s)) {
return 9;
} else if ("ten".equals(s)) {
return 10;
} else if ("eleven".equals(s)) {
return 11;
} else if ("twelve".equals(s)) {
return 12;
}
}
return null;
}
private static ZonedDateTime ymd(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
0, 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
int halfDay = RepeaterDay.DAY_SECONDS / 2;
int fullDay = RepeaterDay.DAY_SECONDS;
ZonedDateTime now = getNow();
Tick tick = getType();
boolean first = false;
if (currentTime == null) {
first = true;
ZonedDateTime midnight = ymd(now);
ZonedDateTime yesterdayMidnight = midnight.minus(fullDay, ChronoUnit.SECONDS);
ZonedDateTime tomorrowMidnight = midnight.plus(fullDay, ChronoUnit.SECONDS);
boolean done = false;
if (pointer == PointerType.FUTURE) {
if (tick.isAmbiguous()) {
List<ZonedDateTime> futureDates = new LinkedList<>();
futureDates.add(midnight.plus(tick.intValue(), ChronoUnit.SECONDS));
futureDates.add(midnight.plus(halfDay + tick.intValue(), ChronoUnit.SECONDS));
futureDates.add(tomorrowMidnight.plus(tick.intValue(), ChronoUnit.SECONDS));
for (ZonedDateTime futureDate : futureDates) {
if (futureDate.isAfter(now) || futureDate.equals(now)) {
currentTime = futureDate;
done = true;
break;
}
}
} else {
List<ZonedDateTime> futureDates = new LinkedList<>();
futureDates.add(midnight.plus(tick.intValue(), ChronoUnit.SECONDS));
futureDates.add(tomorrowMidnight.plus(tick.intValue(), ChronoUnit.SECONDS));
for (ZonedDateTime futureDate : futureDates) {
if (futureDate.isAfter(now) || futureDate.equals(now)) {
currentTime = futureDate;
done = true;
break;
}
}
}
} else {
if (tick.isAmbiguous()) {
List<ZonedDateTime> pastDates = new LinkedList<>();
pastDates.add(midnight.plus(halfDay + tick.intValue(), ChronoUnit.SECONDS));
pastDates.add(midnight.plus(tick.intValue(), ChronoUnit.SECONDS));
pastDates.add(yesterdayMidnight.plus(tick.intValue() * 2, ChronoUnit.SECONDS));
for (ZonedDateTime pastDate : pastDates) {
if (pastDate.isBefore(now) || pastDate.equals(now)) {
currentTime = pastDate;
done = true;
break;
}
}
} else {
List<ZonedDateTime> pastDates = new LinkedList<>();
pastDates.add(midnight.plus(tick.intValue(), ChronoUnit.SECONDS));
pastDates.add(yesterdayMidnight.plus(tick.intValue(), ChronoUnit.SECONDS));
for (ZonedDateTime pastDate : pastDates) {
if (pastDate.isBefore(now) || pastDate.equals(now)) {
currentTime = pastDate;
done = true;
break;
}
}
}
}
if (!done && currentTime == null) {
throw new IllegalStateException("Current time cannot be null at this point.");
}
}
if (!first) {
int increment = (tick.isAmbiguous()) ? halfDay : fullDay;
int direction = (pointer == PointerType.FUTURE) ? 1 : -1;
currentTime = currentTime.plus(direction * increment, ChronoUnit.SECONDS);
}
return new Span(currentTime, currentTime.plus(getWidth(), ChronoUnit.SECONDS));
}
@Override
protected Span internalThisSpan(PointerType pointer) {
if (pointer == PointerType.NONE) {
pointer = PointerType.FUTURE;
}
return nextSpan(pointer);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
throw new IllegalStateException("Not implemented.");
}
@Override
public int getWidth() {
return 1;
}
@Override
public String toString() {
return super.toString() + "-time-" + getType();
}
}

View file

@ -0,0 +1,61 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public abstract class RepeaterUnit extends Repeater<Object> {
private static final Pattern YEAR_PATTERN = Pattern.compile("^years?$");
private static final Pattern MONTH_PATTERN = Pattern.compile("^months?$");
private static final Pattern FORTNIGHT_PATTERN = Pattern.compile("^fortnights?$");
private static final Pattern WEEK_PATTERN = Pattern.compile("^weeks?$");
private static final Pattern WEEKEND_PATTERN = Pattern.compile("^weekends?$");
private static final Pattern DAY_PATTERN = Pattern.compile("^days?$");
private static final Pattern HOUR_PATTERN = Pattern.compile("^hours?$");
private static final Pattern MINUTE_PATTERN = Pattern.compile("^minutes?$");
private static final Pattern SECOND_PATTERN = Pattern.compile("^seconds?$");
public RepeaterUnit() {
super(null);
}
public static RepeaterUnit scan(Token token) {
try {
Map<Pattern, UnitName> scanner = new HashMap<>();
scanner.put(RepeaterUnit.YEAR_PATTERN, UnitName.YEAR);
scanner.put(RepeaterUnit.MONTH_PATTERN, UnitName.MONTH);
scanner.put(RepeaterUnit.FORTNIGHT_PATTERN, UnitName.FORTNIGHT);
scanner.put(RepeaterUnit.WEEK_PATTERN, UnitName.WEEK);
scanner.put(RepeaterUnit.WEEKEND_PATTERN, UnitName.WEEKEND);
scanner.put(RepeaterUnit.DAY_PATTERN, UnitName.DAY);
scanner.put(RepeaterUnit.HOUR_PATTERN, UnitName.HOUR);
scanner.put(RepeaterUnit.MINUTE_PATTERN, UnitName.MINUTE);
scanner.put(RepeaterUnit.SECOND_PATTERN, UnitName.SECOND);
for (Map.Entry<Pattern, UnitName> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
UnitName unitNameEnum = scanner.get(scannerItem);
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 null;
} catch (Throwable t) {
throw new RuntimeException("Failed to create RepeaterUnit.", t);
}
}
/**
*
*/
private enum UnitName {
YEAR, MONTH, FORTNIGHT, WEEK, WEEKEND, DAY, HOUR, MINUTE, SECOND
}
}

View file

@ -0,0 +1,96 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterWeek extends RepeaterUnit {
public static final int WEEK_SECONDS = 604800; // (7 * 24 * 60 * 60);
public static final int WEEK_DAYS = 7;
private ZonedDateTime currentWeekStart;
private static ZonedDateTime ymdh(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
zonedDateTime.getHour(), 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentWeekStart == null) {
if (pointer == PointerType.FUTURE) {
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
Span nextSundaySpan = sundayRepeater.nextSpan(PointerType.FUTURE);
currentWeekStart = nextSundaySpan.getBeginCalendar();
} else if (pointer == PointerType.PAST) {
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow().plus(1, ChronoUnit.DAYS));
sundayRepeater.nextSpan(PointerType.PAST);
Span lastSundaySpan = sundayRepeater.nextSpan(PointerType.PAST);
currentWeekStart = lastSundaySpan.getBeginCalendar();
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
} else {
int direction = (pointer == PointerType.FUTURE) ? 1 : -1;
currentWeekStart = currentWeekStart.plus(RepeaterWeek.WEEK_DAYS * direction, ChronoUnit.DAYS);
}
return new Span(currentWeekStart, ChronoUnit.DAYS, RepeaterWeek.WEEK_DAYS);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
Span thisWeekSpan;
ZonedDateTime thisWeekStart;
ZonedDateTime thisWeekEnd;
if (pointer == PointerType.FUTURE) {
thisWeekStart = ymdh(getNow()).plus(1, ChronoUnit.HOURS);
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
Span thisSundaySpan = sundayRepeater.thisSpan(PointerType.FUTURE);
thisWeekEnd = thisSundaySpan.getBeginCalendar();
thisWeekSpan = new Span(thisWeekStart, thisWeekEnd);
} else if (pointer == PointerType.PAST) {
thisWeekEnd = ymdh(getNow());
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
Span lastSundaySpan = sundayRepeater.nextSpan(PointerType.PAST);
thisWeekStart = lastSundaySpan.getBeginCalendar();
thisWeekSpan = new Span(thisWeekStart, thisWeekEnd);
} else if (pointer == PointerType.NONE) {
RepeaterDayName sundayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SUNDAY);
sundayRepeater.setNow(getNow());
Span lastSundaySpan = sundayRepeater.nextSpan(PointerType.PAST);
thisWeekStart = lastSundaySpan.getBeginCalendar();
thisWeekEnd = thisWeekStart.plus(RepeaterWeek.WEEK_DAYS, ChronoUnit.DAYS);
thisWeekSpan = new Span(thisWeekStart, thisWeekEnd);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return thisWeekSpan;
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
return span.add(direction * amount * RepeaterWeek.WEEK_SECONDS);
}
@Override
public int getWidth() {
return RepeaterWeek.WEEK_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-week";
}
}

View file

@ -0,0 +1,80 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterWeekend extends RepeaterUnit {
public static final long WEEKEND_SECONDS = 172800L; // (2 * 24 * 60 * 60);
private ZonedDateTime currentWeekStart;
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentWeekStart == null) {
if (pointer == PointerType.FUTURE) {
RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY);
saturdayRepeater.setNow(getNow());
Span nextSaturdaySpan = saturdayRepeater.nextSpan(PointerType.FUTURE);
currentWeekStart = nextSaturdaySpan.getBeginCalendar();
} else if (pointer == PointerType.PAST) {
RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY);
saturdayRepeater.setNow(getNow().plus(RepeaterDay.DAY_SECONDS, ChronoUnit.SECONDS));
Span lastSaturdaySpan = saturdayRepeater.nextSpan(PointerType.PAST);
currentWeekStart = lastSaturdaySpan.getBeginCalendar();
}
} else {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
currentWeekStart = currentWeekStart.plus(direction * RepeaterWeek.WEEK_SECONDS, ChronoUnit.SECONDS);
}
assert currentWeekStart != null;
ZonedDateTime c = currentWeekStart.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS);
return new Span(currentWeekStart, c);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
Span thisSpan;
if (pointer == PointerType.FUTURE || pointer == PointerType.NONE) {
RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY);
saturdayRepeater.setNow(getNow());
Span thisSaturdaySpan = saturdayRepeater.nextSpan(PointerType.FUTURE);
thisSpan = new Span(thisSaturdaySpan.getBeginCalendar(), thisSaturdaySpan.getBeginCalendar()
.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS));
} else if (pointer == PointerType.PAST) {
RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY);
saturdayRepeater.setNow(getNow());
Span lastSaturdaySpan = saturdayRepeater.nextSpan(PointerType.PAST);
thisSpan = new Span(lastSaturdaySpan.getBeginCalendar(), lastSaturdaySpan.getBeginCalendar()
.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS));
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return thisSpan;
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long direction = pointer == PointerType.FUTURE ? 1L : -1L;
RepeaterWeekend weekend = new RepeaterWeekend();
weekend.setNow(span.getBeginCalendar());
ZonedDateTime start = weekend.nextSpan(pointer).getBeginCalendar().plus((amount - 1) *
direction * RepeaterWeek.WEEK_SECONDS, ChronoUnit.SECONDS);
return new Span(start, start.plus(span.getWidth(), ChronoUnit.SECONDS));
}
@Override
public int getWidth() {
return (int) RepeaterWeekend.WEEKEND_SECONDS;
}
@Override
public String toString() {
return super.toString() + "-weekend";
}
}

View file

@ -0,0 +1,77 @@
package org.xbib.time.chronic.repeaters;
import org.xbib.time.chronic.Span;
import org.xbib.time.chronic.tags.Pointer.PointerType;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
*
*/
public class RepeaterYear extends RepeaterUnit {
private ZonedDateTime currentYearStart;
private static ZonedDateTime ymd(ZonedDateTime zonedDateTime) {
return ZonedDateTime.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth(),
0, 0, 0, 0, zonedDateTime.getZone());
}
@Override
protected Span internalNextSpan(PointerType pointer) {
if (currentYearStart == null) {
if (pointer == PointerType.FUTURE) {
currentYearStart = ZonedDateTime.of(getNow().getYear(), 1, 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.YEARS);
} else if (pointer == PointerType.PAST) {
currentYearStart = ZonedDateTime.of(getNow().getYear(), 1, 1, 0, 0, 0, 0, getNow().getZone())
.minus(1, ChronoUnit.YEARS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
} else {
int direction = (pointer == PointerType.FUTURE) ? 1 : -1;
currentYearStart = currentYearStart.plus(direction, ChronoUnit.YEARS);
}
return new Span(currentYearStart, ChronoUnit.YEARS, 1);
}
@Override
protected Span internalThisSpan(PointerType pointer) {
ZonedDateTime yearStart;
ZonedDateTime yearEnd;
if (pointer == PointerType.FUTURE) {
yearStart = ymd(getNow()).plus(1, ChronoUnit.DAYS);
yearEnd = ZonedDateTime.of(getNow().getYear(), 1, 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.YEARS);
} else if (pointer == PointerType.PAST) {
yearStart = ZonedDateTime.of(getNow().getYear(), 1, 1, 0, 0, 0, 0, getNow().getZone());
yearEnd = ymd(getNow());
} else if (pointer == PointerType.NONE) {
yearStart = ZonedDateTime.of(getNow().getYear(), 1, 1, 0, 0, 0, 0, getNow().getZone());
yearEnd = ZonedDateTime.of(getNow().getYear(), 1, 1, 0, 0, 0, 0, getNow().getZone())
.plus(1, ChronoUnit.YEARS);
} else {
throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
}
return new Span(yearStart, yearEnd);
}
@Override
public Span getOffset(Span span, int amount, PointerType pointer) {
long l = amount * (pointer == PointerType.FUTURE ? 1L : -1L);
ZonedDateTime newBegin = span.getBeginCalendar().plus(l, ChronoUnit.YEARS);
ZonedDateTime newEnd = span.getEndCalendar().plus(l, ChronoUnit.YEARS);
return new Span(newBegin, newEnd);
}
@Override
public int getWidth() {
return (365 * 24 * 60 * 60);
}
@Override
public String toString() {
return super.toString() + "-year";
}
}

View file

@ -0,0 +1,4 @@
/**
* Classes for chronic repeaters.
*/
package org.xbib.time.chronic.repeaters;

View file

@ -0,0 +1,58 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class Grabber extends Tag<Grabber.Relative> {
private static final Pattern THIS_PATTERN = Pattern.compile("this");
private static final Pattern NEXT_PATTERN = Pattern.compile("next");
private static final Pattern LAST_PATTERN = Pattern.compile("last");
public Grabber(Relative type) {
super(type);
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (Token token : tokens) {
Grabber t = Grabber.scanForAll(token, options);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
public static Grabber scanForAll(Token token, Options options) {
Map<Pattern, Relative> scanner = new HashMap<>();
scanner.put(Grabber.LAST_PATTERN, Relative.LAST);
scanner.put(Grabber.NEXT_PATTERN, Relative.NEXT);
scanner.put(Grabber.THIS_PATTERN, Relative.THIS);
for (Map.Entry<Pattern, Relative> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new Grabber(scanner.get(scannerItem));
}
}
return null;
}
@Override
public String toString() {
return "grabber-" + getType();
}
/**
*
*/
public enum Relative {
LAST, NEXT, THIS
}
}

View file

@ -0,0 +1,48 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*/
public class Ordinal extends Tag<Integer> {
static final Pattern ORDINAL_PATTERN = Pattern.compile("^(\\d*)(st|nd|rd|th)$");
Ordinal(Integer type) {
super(type);
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (Token token : tokens) {
Ordinal t;
t = Ordinal.scan(token, options);
if (t != null) {
token.tag(t);
}
t = OrdinalDay.scan(token);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
public static Ordinal scan(Token token, Options options) {
Matcher ordinalMatcher = ORDINAL_PATTERN.matcher(token.getWord());
if (ordinalMatcher.find()) {
return new Ordinal(Integer.valueOf(ordinalMatcher.group(1)));
}
return null;
}
@Override
public String toString() {
return "ordinal";
}
}

View file

@ -0,0 +1,30 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Token;
import java.util.regex.Matcher;
/**
*
*/
public class OrdinalDay extends Ordinal {
public OrdinalDay(Integer type) {
super(type);
}
public static OrdinalDay scan(Token token) {
Matcher ordinalMatcher = Ordinal.ORDINAL_PATTERN.matcher(token.getWord());
if (ordinalMatcher.find()) {
int ordinalValue = Integer.parseInt(ordinalMatcher.group(1));
if (!(ordinalValue > 31)) {
return new OrdinalDay(ordinalValue);
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-day-" + getType();
}
}

View file

@ -0,0 +1,61 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class Pointer extends Tag<Pointer.PointerType> {
private static final Pattern IN_PATTERN = Pattern.compile("\\bin\\b");
private static final Pattern FUTURE_PATTERN = Pattern.compile("\\bfuture\\b");
private static final Pattern PAST_PATTERN = Pattern.compile("\\bpast\\b");
public Pointer(PointerType type) {
super(type);
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (Token token : tokens) {
Pointer t = Pointer.scanForAll(token, options);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
public static Pointer scanForAll(Token token, Options options) {
Map<Pattern, PointerType> scanner = new HashMap<>();
scanner.put(Pointer.PAST_PATTERN, PointerType.PAST);
scanner.put(Pointer.FUTURE_PATTERN, PointerType.FUTURE);
scanner.put(Pointer.IN_PATTERN, PointerType.FUTURE);
for (Map.Entry<Pattern, PointerType> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new Pointer(scanner.get(scannerItem));
}
}
return null;
}
@Override
public String toString() {
return "pointer-" + getType();
}
/**
*
*/
public enum PointerType {
PAST, FUTURE, NONE
}
}

View file

@ -0,0 +1,103 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
*
*/
public class Scalar extends Tag<Integer> {
static final Set<String> TIMES =
new HashSet<>(Arrays.asList("am", "pm", "morning", "afternoon", "evening", "night"));
private static final Pattern SCALAR_PATTERN = Pattern.compile("^\\d*$");
public Scalar(Integer type) {
super(type);
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (int i = 0; i < tokens.size(); i++) {
Token token = tokens.get(i);
Token postToken = null;
if (i < tokens.size() - 1) {
postToken = tokens.get(i + 1);
}
Scalar t;
t = Scalar.scan(token, postToken, options);
if (t != null) {
token.tag(t);
}
t = ScalarDay.scan(token, postToken, options);
if (t != null) {
token.tag(t);
}
t = ScalarMonth.scan(token, postToken, options);
if (t != null) {
token.tag(t);
}
t = ScalarYear.scan(token, postToken, options);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
public static Scalar scan(Token token, Token postToken, Options options) {
if (Scalar.SCALAR_PATTERN.matcher(token.getWord()).matches()) {
if (token.getWord() != null && token.getWord().length() > 0 && !(postToken != null &&
Scalar.TIMES.contains(postToken.getWord()))) {
return new Scalar(Integer.valueOf(token.getWord()));
}
} else {
Integer intStrValue = integerValue(token.getWord());
if (intStrValue != null) {
return new Scalar(intStrValue);
}
}
return null;
}
private static Integer integerValue(String str) {
if (str != null) {
String s = str.toLowerCase();
if ("one".equals(s)) {
return 1;
} else if ("two".equals(s)) {
return 2;
} else if ("three".equals(s)) {
return 3;
} else if ("four".equals(s)) {
return 4;
} else if ("five".equals(s)) {
return 5;
} else if ("six".equals(s)) {
return 6;
} else if ("seven".equals(s)) {
return 7;
} else if ("eight".equals(s)) {
return 8;
} else if ("nine".equals(s)) {
return 9;
} else if ("ten".equals(s)) {
return 10;
} else if ("eleven".equals(s)) {
return 11;
} else if ("twelve".equals(s)) {
return 12;
}
}
return null;
}
@Override
public String toString() {
return "scalar";
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.regex.Pattern;
/**
*
*/
public class ScalarDay extends Scalar {
private static final Pattern DAY_PATTERN = Pattern.compile("^\\d\\d?$");
public ScalarDay(Integer type) {
super(type);
}
public static ScalarDay scan(Token token, Token postToken, Options options) {
if (ScalarDay.DAY_PATTERN.matcher(token.getWord()).matches()) {
int scalarValue = Integer.parseInt(token.getWord());
if (!(scalarValue > 31 || (postToken != null && Scalar.TIMES.contains(postToken.getWord())))) {
return new ScalarDay(scalarValue);
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-day-" + getType();
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.regex.Pattern;
/**
*
*/
public class ScalarMonth extends Scalar {
private static final Pattern MONTH_PATTERN = Pattern.compile("^\\d\\d?$");
public ScalarMonth(Integer type) {
super(type);
}
public static ScalarMonth scan(Token token, Token postToken, Options options) {
if (ScalarMonth.MONTH_PATTERN.matcher(token.getWord()).matches()) {
int scalarValue = Integer.parseInt(token.getWord());
if (!(scalarValue > 12 || (postToken != null && Scalar.TIMES.contains(postToken.getWord())))) {
return new ScalarMonth(scalarValue);
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-month-" + getType();
}
}

View file

@ -0,0 +1,37 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.regex.Pattern;
/**
*
*/
public class ScalarYear extends Scalar {
private static final Pattern YEAR_PATTERN = Pattern.compile("^([1-9]\\d)?\\d\\d?$");
private ScalarYear(Integer type) {
super(type);
}
public static ScalarYear scan(Token token, Token postToken, Options options) {
if (ScalarYear.YEAR_PATTERN.matcher(token.getWord()).matches()) {
int scalarValue = Integer.parseInt(token.getWord());
if (!(postToken != null && Scalar.TIMES.contains(postToken.getWord()))) {
if (scalarValue <= 37) {
scalarValue += 2000;
} else if (scalarValue <= 137 && scalarValue >= 69) {
scalarValue += 1900;
}
return new ScalarYear(scalarValue);
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-year-" + getType();
}
}

View file

@ -0,0 +1,51 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.List;
/**
*
*/
public class Separator extends Tag<Separator.SeparatorType> {
Separator(SeparatorType type) {
super(type);
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (Token token : tokens) {
Separator t;
t = SeparatorComma.scan(token, options);
if (t != null) {
token.tag(t);
}
t = SeparatorSlashOrDash.scan(token, options);
if (t != null) {
token.tag(t);
}
t = SeparatorAt.scan(token, options);
if (t != null) {
token.tag(t);
}
t = SeparatorIn.scan(token, options);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
@Override
public String toString() {
return "separator";
}
/**
*
*/
enum SeparatorType {
COMMA, DASH, SLASH, AT, IN
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class SeparatorAt extends Separator {
private static final Pattern AT_PATTERN = Pattern.compile("^(at|@)$");
private SeparatorAt(SeparatorType type) {
super(type);
}
public static SeparatorAt scan(Token token, Options options) {
Map<Pattern, SeparatorType> scanner = new HashMap<>();
scanner.put(SeparatorAt.AT_PATTERN, SeparatorType.AT);
for (Map.Entry<Pattern, SeparatorType> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new SeparatorAt(scanner.get(scannerItem));
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-at";
}
}

View file

@ -0,0 +1,37 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class SeparatorComma extends Separator {
private static final Pattern COMMA_PATTERN = Pattern.compile("^,$");
private SeparatorComma(SeparatorType type) {
super(type);
}
public static SeparatorComma scan(Token token, Options options) {
Map<Pattern, SeparatorType> scanner = new HashMap<>();
scanner.put(SeparatorComma.COMMA_PATTERN, SeparatorType.COMMA);
for (Map.Entry<Pattern, SeparatorType> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new SeparatorComma(scanner.get(scannerItem));
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-comma";
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class SeparatorIn extends Separator {
private static final Pattern IN_PATTERN = Pattern.compile("^in$");
public SeparatorIn(SeparatorType type) {
super(type);
}
public static SeparatorIn scan(Token token, Options options) {
Map<Pattern, SeparatorType> scanner = new HashMap<>();
scanner.put(SeparatorIn.IN_PATTERN, SeparatorType.IN);
for (Map.Entry<Pattern, SeparatorType> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new SeparatorIn(scanner.get(scannerItem));
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-in";
}
}

View file

@ -0,0 +1,38 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class SeparatorSlashOrDash extends Separator {
private static final Pattern SLASH_PATTERN = Pattern.compile("^/$");
private static final Pattern DASH_PATTERN = Pattern.compile("^-$");
public SeparatorSlashOrDash(SeparatorType type) {
super(type);
}
public static SeparatorSlashOrDash scan(Token token, Options options) {
Map<Pattern, SeparatorType> scanner = new HashMap<>();
scanner.put(SeparatorSlashOrDash.DASH_PATTERN, SeparatorType.DASH);
scanner.put(SeparatorSlashOrDash.SLASH_PATTERN, SeparatorType.SLASH);
for (Map.Entry<Pattern, SeparatorType> entry : scanner.entrySet()) {
Pattern scannerItem = entry.getKey();
if (scannerItem.matcher(token.getWord()).matches()) {
return new SeparatorSlashOrDash(scanner.get(scannerItem));
}
}
return null;
}
@Override
public String toString() {
return super.toString() + "-slashordash-" + getType();
}
}

View file

@ -0,0 +1,10 @@
package org.xbib.time.chronic.tags;
/**
*
*/
public class StringTag extends Tag<String> {
public StringTag(String type) {
super(type);
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.time.chronic.tags;
import java.time.ZonedDateTime;
/**
* Tokens are tagged with subclassed instances of this class when
* they match specific criteria.
* @param <T> type parameter
*/
public class Tag<T> {
private T type;
private ZonedDateTime now;
public Tag(T type) {
this.type = type;
}
public T getType() {
return type;
}
public void setType(T type) {
this.type = type;
}
public ZonedDateTime getNow() {
return now;
}
public void setNow(ZonedDateTime s) {
this.now = s;
}
}

View file

@ -0,0 +1,46 @@
package org.xbib.time.chronic.tags;
import org.xbib.time.chronic.Options;
import org.xbib.time.chronic.Token;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
*
*/
public class TimeZone extends Tag<Object> {
private static final Pattern TIMEZONE_PATTERN = Pattern.compile("[pmce][ds]t");
private TimeZone() {
super(null);
}
public static List<Token> scan(List<Token> tokens, Options options) {
for (Token token : tokens) {
TimeZone t = TimeZone.scanForAll(token, options);
if (t != null) {
token.tag(t);
}
}
return tokens;
}
private static TimeZone scanForAll(Token token, Options options) {
Map<Pattern, Object> scanner = new HashMap<>();
scanner.put(TimeZone.TIMEZONE_PATTERN, null);
for (Pattern scannerItem : scanner.keySet()) {
if (scannerItem.matcher(token.getWord()).matches()) {
return new TimeZone();
}
}
return null;
}
@Override
public String toString() {
return "timezone";
}
}

View file

@ -0,0 +1,4 @@
/**
* Classes for chronic tags.
*/
package org.xbib.time.chronic.tags;

View file

@ -0,0 +1,430 @@
package org.xbib.time.format;
import java.io.IOException;
import java.io.Writer;
/**
* Utility methods used by formatters.
* FormatUtils is thread-safe and immutable.
*/
public class FormatUtils {
private static final double LOG_10 = Math.log(10);
/**
* Restricted constructor.
*/
private FormatUtils() {
}
/**
* Converts an integer to a string, prepended with a variable amount of '0'
* pad characters, and appends it to the given buffer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param buf receives integer converted to a string
* @param value value to convert to a string
* @param size minimum amount of digits to append
*/
public static void appendPaddedInteger(StringBuilder buf, int value, int size) {
try {
appendPaddedInteger((Appendable) buf, value, size);
} catch (IOException e) {
// StringBuilder does not throw IOException
}
}
/**
* Converts an integer to a string, prepended with a variable amount of '0'
* pad characters, and appends it to the given appendable.
* <p>
* This method is optimized for converting small values to strings.
*
* @param appenadble receives integer converted to a string
* @param value value to convert to a string
* @param size minimum amount of digits to append
* @throws IOException exception
*/
public static void appendPaddedInteger(Appendable appenadble, int value, int size) throws IOException {
if (value < 0) {
appenadble.append('-');
if (value != Integer.MIN_VALUE) {
value = -value;
} else {
for (; size > 10; size--) {
appenadble.append('0');
}
appenadble.append("" + -(long) Integer.MIN_VALUE);
return;
}
}
if (value < 10) {
for (; size > 1; size--) {
appenadble.append('0');
}
appenadble.append((char) (value + '0'));
} else if (value < 100) {
for (; size > 2; size--) {
appenadble.append('0');
}
// Calculate value div/mod by 10 without using two expensive
// division operations. (2 ^ 27) / 10 = 13421772. Add one to
// value to correct rounding error.
int d = ((value + 1) * 13421772) >> 27;
appenadble.append((char) (d + '0'));
// Append remainder by calculating (value - d * 10).
appenadble.append((char) (value - (d << 3) - (d << 1) + '0'));
} else {
int digits;
if (value < 1000) {
digits = 3;
} else if (value < 10000) {
digits = 4;
} else {
digits = (int) (Math.log(value) / LOG_10) + 1;
}
for (; size > digits; size--) {
appenadble.append('0');
}
appenadble.append(Integer.toString(value));
}
}
/**
* Converts an integer to a string, prepended with a variable amount of '0'
* pad characters, and appends it to the given buffer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param buf receives integer converted to a string
* @param value value to convert to a string
* @param size minimum amount of digits to append
*/
public static void appendPaddedInteger(StringBuilder buf, long value, int size) {
try {
appendPaddedInteger((Appendable) buf, value, size);
} catch (IOException e) {
// StringBuilder does not throw IOException
}
}
/**
* Converts an integer to a string, prepended with a variable amount of '0'
* pad characters, and appends it to the given buffer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param appendable receives integer converted to a string
* @param value value to convert to a string
* @param size minimum amount of digits to append
* @throws IOException exception
*/
public static void appendPaddedInteger(Appendable appendable, long value, int size) throws IOException {
int intValue = (int) value;
if (intValue == value) {
appendPaddedInteger(appendable, intValue, size);
} else if (size <= 19) {
appendable.append(Long.toString(value));
} else {
if (value < 0) {
appendable.append('-');
if (value != Long.MIN_VALUE) {
value = -value;
} else {
for (; size > 19; size--) {
appendable.append('0');
}
appendable.append("9223372036854775808");
return;
}
}
int digits = (int) (Math.log(value) / LOG_10) + 1;
for (; size > digits; size--) {
appendable.append('0');
}
appendable.append(Long.toString(value));
}
}
/**
* Converts an integer to a string, prepended with a variable amount of '0'
* pad characters, and writes it to the given writer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param out receives integer converted to a string
* @param value value to convert to a string
* @param size minimum amount of digits to append
* @throws IOException exception
*/
public static void writePaddedInteger(Writer out, int value, int size)
throws IOException {
if (value < 0) {
out.write('-');
if (value != Integer.MIN_VALUE) {
value = -value;
} else {
for (; size > 10; size--) {
out.write('0');
}
out.write("" + -(long) Integer.MIN_VALUE);
return;
}
}
if (value < 10) {
for (; size > 1; size--) {
out.write('0');
}
out.write(value + '0');
} else if (value < 100) {
for (; size > 2; size--) {
out.write('0');
}
// Calculate value div/mod by 10 without using two expensive
// division operations. (2 ^ 27) / 10 = 13421772. Add one to
// value to correct rounding error.
int d = ((value + 1) * 13421772) >> 27;
out.write(d + '0');
// Append remainder by calculating (value - d * 10).
out.write(value - (d << 3) - (d << 1) + '0');
} else {
int digits;
if (value < 1000) {
digits = 3;
} else if (value < 10000) {
digits = 4;
} else {
digits = (int) (Math.log(value) / LOG_10) + 1;
}
for (; size > digits; size--) {
out.write('0');
}
out.write(Integer.toString(value));
}
}
/**
* Converts an integer to a string, prepended with a variable amount of '0'
* pad characters, and writes it to the given writer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param out receives integer converted to a string
* @param value value to convert to a string
* @param size minimum amount of digits to append
* @throws IOException exception
*/
public static void writePaddedInteger(Writer out, long value, int size)
throws IOException {
int intValue = (int) value;
if (intValue == value) {
writePaddedInteger(out, intValue, size);
} else if (size <= 19) {
out.write(Long.toString(value));
} else {
if (value < 0) {
out.write('-');
if (value != Long.MIN_VALUE) {
value = -value;
} else {
for (; size > 19; size--) {
out.write('0');
}
out.write("9223372036854775808");
return;
}
}
int digits = (int) (Math.log(value) / LOG_10) + 1;
for (; size > digits; size--) {
out.write('0');
}
out.write(Long.toString(value));
}
}
/**
* Converts an integer to a string, and appends it to the given buffer.
* <p>This method is optimized for converting small values to strings.
*
* @param buf receives integer converted to a string
* @param value value to convert to a string
*/
public static void appendUnpaddedInteger(StringBuilder buf, int value) {
try {
appendUnpaddedInteger((Appendable) buf, value);
} catch (IOException e) {
// StringBuilder do not throw IOException
}
}
/**
* Converts an integer to a string, and appends it to the given appendable.
* <p>
* This method is optimized for converting small values to strings.
*
* @param appendable receives integer converted to a string
* @param value value to convert to a string
* @throws IOException exception
*/
public static void appendUnpaddedInteger(Appendable appendable, int value) throws IOException {
if (value < 0) {
appendable.append('-');
if (value != Integer.MIN_VALUE) {
value = -value;
} else {
appendable.append("" + -(long) Integer.MIN_VALUE);
return;
}
}
if (value < 10) {
appendable.append((char) (value + '0'));
} else if (value < 100) {
// Calculate value div/mod by 10 without using two expensive
// division operations. (2 ^ 27) / 10 = 13421772. Add one to
// value to correct rounding error.
int d = ((value + 1) * 13421772) >> 27;
appendable.append((char) (d + '0'));
// Append remainder by calculating (value - d * 10).
appendable.append((char) (value - (d << 3) - (d << 1) + '0'));
} else {
appendable.append(Integer.toString(value));
}
}
/**
* Converts an integer to a string, and appends it to the given buffer.
* <p>This method is optimized for converting small values to strings.
*
* @param buf receives integer converted to a string
* @param value value to convert to a string
*/
public static void appendUnpaddedInteger(StringBuilder buf, long value) {
try {
appendUnpaddedInteger((Appendable) buf, value);
} catch (IOException e) {
// StringBuilder do not throw IOException
}
}
/**
* Converts an integer to a string, and appends it to the given appendable.
* <p>
* This method is optimized for converting small values to strings.
*
* @param appendable receives integer converted to a string
* @param value value to convert to a string
* @throws IOException exception
*/
public static void appendUnpaddedInteger(Appendable appendable, long value) throws IOException {
int intValue = (int) value;
if (intValue == value) {
appendUnpaddedInteger(appendable, intValue);
} else {
appendable.append(Long.toString(value));
}
}
/**
* Converts an integer to a string, and writes it to the given writer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param out receives integer converted to a string
* @param value value to convert to a string
* @throws IOException exception
*/
public static void writeUnpaddedInteger(Writer out, int value)
throws IOException {
if (value < 0) {
out.write('-');
if (value != Integer.MIN_VALUE) {
value = -value;
} else {
out.write("" + -(long) Integer.MIN_VALUE);
return;
}
}
if (value < 10) {
out.write(value + '0');
} else if (value < 100) {
// Calculate value div/mod by 10 without using two expensive
// division operations. (2 ^ 27) / 10 = 13421772. Add one to
// value to correct rounding error.
int d = ((value + 1) * 13421772) >> 27;
out.write(d + '0');
// Append remainder by calculating (value - d * 10).
out.write(value - (d << 3) - (d << 1) + '0');
} else {
out.write(Integer.toString(value));
}
}
/**
* Converts an integer to a string, and writes it to the given writer.
* <p>
* This method is optimized for converting small values to strings.
*
* @param out receives integer converted to a string
* @param value value to convert to a string
* @throws IOException exception
*/
public static void writeUnpaddedInteger(Writer out, long value)
throws IOException {
int intValue = (int) value;
if (intValue == value) {
writeUnpaddedInteger(out, intValue);
} else {
out.write(Long.toString(value));
}
}
/**
* Calculates the number of decimal digits for the given value,
* including the sign.
*
* @param value value
* @return the number of decimal digits
*/
public static int calculateDigitCount(long value) {
if (value < 0) {
if (value != Long.MIN_VALUE) {
return calculateDigitCount(-value) + 1;
} else {
return 20;
}
}
return
(value < 10 ? 1 :
(value < 100 ? 2 :
(value < 1000 ? 3 :
(value < 10000 ? 4 :
((int) (Math.log(value) / LOG_10) + 1)))));
}
static int parseTwoDigits(CharSequence text, int position) {
int value = text.charAt(position) - '0';
return ((value << 3) + (value << 1)) + text.charAt(position + 1) - '0';
}
static String createErrorMessage(final String text, final int errorPos) {
int sampleLen = errorPos + 32;
String sampleText;
if (text.length() <= sampleLen + 3) {
sampleText = text;
} else {
sampleText = text.substring(0, sampleLen).concat("...");
}
if (errorPos <= 0) {
return "Invalid format: \"" + sampleText + '"';
}
if (errorPos >= text.length()) {
return "Invalid format: \"" + sampleText + "\" is too short";
}
return "Invalid format: \"" + sampleText + "\" is malformed at \"" +
sampleText.substring(errorPos) + '"';
}
}

View file

@ -0,0 +1,190 @@
package org.xbib.time.format;
/**
* Factory that creates instances of PeriodFormatter for the ISO8601 standard.
* Period formatting is performed by the {@link PeriodFormatter} class.
* Three classes provide factory methods to create formatters, and this is one.
* The others are {@link PeriodFormat} and {@link PeriodFormatterBuilder}.
* ISOPeriodFormat is thread-safe and immutable, and the formatters it
* returns are as well.
*/
public class ISOPeriodFormat {
/**
* Cache of standard format.
*/
private static PeriodFormatter cStandard;
/**
* Cache of alternate months format.
*/
private static PeriodFormatter cAlternate;
/**
* Cache of alternate extended months format.
*/
private static PeriodFormatter cAlternateExtended;
/**
* Cache of alternate weeks format.
*/
private static PeriodFormatter cAlternateWithWeeks;
/**
* Cache of alternate extended weeks format.
*/
private static PeriodFormatter cAlternateExtendedWihWeeks;
/**
* Constructor.
*/
protected ISOPeriodFormat() {
super();
}
/**
* The standard ISO format - PyYmMwWdDThHmMsS.
* Milliseconds are not output.
* Note that the ISO8601 standard actually indicates weeks should not
* be shown if any other field is present and vice versa.
*
* @return the formatter
*/
public static PeriodFormatter standard() {
if (cStandard == null) {
cStandard = new PeriodFormatterBuilder()
.appendLiteral("P")
.appendYears()
.appendSuffix("Y")
.appendMonths()
.appendSuffix("M")
.appendWeeks()
.appendSuffix("W")
.appendDays()
.appendSuffix("D")
.appendSeparatorIfFieldsAfter("T")
.appendHours()
.appendSuffix("H")
.appendMinutes()
.appendSuffix("M")
.appendSeconds()
.appendSuffix("S")
.toFormatter();
}
return cStandard;
}
/**
* The alternate ISO format, PyyyymmddThhmmss, which excludes weeks.
* <p>
* Even if weeks are present in the period, they are not output.
* Fractional seconds (milliseconds) will appear if required.
*
* @return the formatter
*/
public static PeriodFormatter alternate() {
if (cAlternate == null) {
cAlternate = new PeriodFormatterBuilder()
.appendLiteral("P")
.minimumPrintedDigits(4)
.appendYears()
.minimumPrintedDigits(2)
.appendMonths()
.appendDays()
.appendSeparatorIfFieldsAfter("T")
.appendHours()
.appendMinutes()
.appendSeconds()
.appendMillis()
.toFormatter();
}
return cAlternate;
}
/**
* The alternate ISO format, Pyyyy-mm-ddThh:mm:ss, which excludes weeks.
* <p>
* Even if weeks are present in the period, they are not output.
* Fractional seconds (milliseconds) will appear if required.
*
* @return the formatter
*/
public static PeriodFormatter alternateExtended() {
if (cAlternateExtended == null) {
cAlternateExtended = new PeriodFormatterBuilder()
.appendLiteral("P")
.minimumPrintedDigits(4)
.appendYears()
.appendSeparator("-")
.minimumPrintedDigits(2)
.appendMonths()
.appendSeparator("-")
.appendDays()
.appendSeparatorIfFieldsAfter("T")
.appendHours()
.appendSeparator(":")
.appendMinutes()
.appendSeparator(":")
.appendSeconds()
.appendMillis()
.toFormatter();
}
return cAlternateExtended;
}
/**
* The alternate ISO format, PyyyyWwwddThhmmss, which excludes months.
* <p>
* Even if months are present in the period, they are not output.
* Fractional seconds (milliseconds) will appear if required.
*
* @return the formatter
*/
public static PeriodFormatter alternateWithWeeks() {
if (cAlternateWithWeeks == null) {
cAlternateWithWeeks = new PeriodFormatterBuilder()
.appendLiteral("P")
.minimumPrintedDigits(4)
.appendYears()
.minimumPrintedDigits(2)
.appendPrefix("W")
.appendWeeks()
.appendDays()
.appendSeparatorIfFieldsAfter("T")
.appendHours()
.appendMinutes()
.appendSeconds()
.appendMillis()
.toFormatter();
}
return cAlternateWithWeeks;
}
/**
* The alternate ISO format, Pyyyy-Www-ddThh:mm:ss, which excludes months.
* <p>
* Even if months are present in the period, they are not output.
* Fractional seconds (milliseconds) will appear if required.
*
* @return the formatter
*/
public static PeriodFormatter alternateExtendedWithWeeks() {
if (cAlternateExtendedWihWeeks == null) {
cAlternateExtendedWihWeeks = new PeriodFormatterBuilder()
.appendLiteral("P")
.minimumPrintedDigits(4)
.appendYears()
.appendSeparator("-")
.minimumPrintedDigits(2)
.appendPrefix("W")
.appendWeeks()
.appendSeparator("-")
.appendDays()
.appendSeparatorIfFieldsAfter("T")
.appendHours()
.appendSeparator(":")
.appendMinutes()
.appendSeparator(":")
.appendSeconds()
.appendMillis()
.toFormatter();
}
return cAlternateExtendedWihWeeks;
}
}

View file

@ -0,0 +1,41 @@
package org.xbib.time.format;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
*/
public class PeriodAmount implements TemporalAmount {
private Map<TemporalUnit, Long> amounts = new HashMap<>();
public void set(TemporalUnit unit, Long value) {
amounts.put(unit, value);
}
@Override
public long get(TemporalUnit unit) {
return amounts.get(unit);
}
@Override
public List<TemporalUnit> getUnits() {
return new ArrayList<>(amounts.keySet());
}
@Override
public Temporal addTo(Temporal temporal) {
return temporal.plus(this);
}
@Override
public Temporal subtractFrom(Temporal temporal) {
return temporal.minus(this);
}
}

View file

@ -0,0 +1,374 @@
package org.xbib.time.format;
import java.io.IOException;
import java.io.Writer;
import java.time.Period;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Factory that creates instances of PeriodFormatter.
* Period formatting is performed by the {@link PeriodFormatter} class.
* Three classes provide factory methods to create formatters, and this is one.
* The others are {@link ISOPeriodFormat} and {@link PeriodFormatterBuilder}.
* PeriodFormat is thread-safe and immutable, and the formatters it returns
* are as well.
*/
public class PeriodFormat {
/**
* The resource bundle name.
*/
private static final String BUNDLE_NAME = "org.xbib.time.format.messages";
/**
* The created formatters.
*/
private static final ConcurrentMap<Locale, PeriodFormatter> FORMATTERS = new ConcurrentHashMap<Locale, PeriodFormatter>();
/**
* Constructor.
*/
protected PeriodFormat() {
super();
}
/**
* Gets the default formatter that outputs words in English.
* This calls {@link #wordBased(Locale)} using a locale of {@code ENGLISH}.
*
* @return the formatter, not null
*/
public static PeriodFormatter getDefault() {
return wordBased(Locale.ENGLISH);
}
/**
* Returns a word based formatter for the JDK default locale.
* This calls {@link #wordBased(Locale)} using the {@link Locale#getDefault() default locale}.
*
* @return the formatter, not null
*/
public static PeriodFormatter wordBased() {
return wordBased(Locale.getDefault());
}
/**
* Returns a word based formatter for the specified locale.
* <p>
* The words are configured in a resource bundle text file -
* {@code org.joda.time.format.messages}.
* This can be added to via the normal classpath resource bundle mechanisms.
* <p>
* You can add your own translation by creating messages_&lt;locale&gt;.properties file
* and adding it to the {@code org.joda.time.format.messages} path.
* <p>
* Simple example (1 = singular suffix, not 1 = plural suffix):
* <pre>
* PeriodFormat.space=\
* PeriodFormat.comma=,
* PeriodFormat.commandand=,and
* PeriodFormat.commaspaceand=, and
* PeriodFormat.commaspace=,
* PeriodFormat.spaceandspace=\ and
* PeriodFormat.year=\ year
* PeriodFormat.years=\ years
* PeriodFormat.month=\ month
* PeriodFormat.months=\ months
* PeriodFormat.week=\ week
* PeriodFormat.weeks=\ weeks
* PeriodFormat.day=\ day
* PeriodFormat.days=\ days
* PeriodFormat.hour=\ hour
* PeriodFormat.hours=\ hours
* PeriodFormat.minute=\ minute
* PeriodFormat.minutes=\ minutes
* PeriodFormat.second=\ second
* PeriodFormat.seconds=\ seconds
* PeriodFormat.millisecond=\ millisecond
* PeriodFormat.milliseconds=\ milliseconds
* </pre>
* <p>
* Some languages contain more than two suffixes. You can use regular expressions
* for them. Here's an example using regular expression for English:
* <pre>
* PeriodFormat.space=\
* PeriodFormat.comma=,
* PeriodFormat.commandand=,and
* PeriodFormat.commaspaceand=, and
* PeriodFormat.commaspace=,
* PeriodFormat.spaceandspace=\ and
* PeriodFormat.regex.separator=%
* PeriodFormat.years.regex=1$%.*
* PeriodFormat.years.list=\ year%\ years
* PeriodFormat.months.regex=1$%.*
* PeriodFormat.months.list=\ month%\ months
* PeriodFormat.weeks.regex=1$%.*
* PeriodFormat.weeks.list=\ week%\ weeks
* PeriodFormat.days.regex=1$%.*
* PeriodFormat.days.list=\ day%\ days
* PeriodFormat.hours.regex=1$%.*
* PeriodFormat.hours.list=\ hour%\ hours
* PeriodFormat.minutes.regex=1$%.*
* PeriodFormat.minutes.list=\ minute%\ minutes
* PeriodFormat.seconds.regex=1$%.*
* PeriodFormat.seconds.list=\ second%\ seconds
* PeriodFormat.milliseconds.regex=1$%.*
* PeriodFormat.milliseconds.list=\ millisecond%\ milliseconds
* </pre>
* <p>
* You can mix both approaches. Here's example for Polish (
* "1 year, 2 years, 5 years, 12 years, 15 years, 21 years, 22 years, 25 years"
* translates to
* "1 rok, 2 lata, 5 lat, 12 lat, 15 lat, 21 lat, 22 lata, 25 lat"). Notice that
* PeriodFormat.day and PeriodFormat.days is used for day suffixes as there is no
* need for regular expressions:
* <pre>
* PeriodFormat.space=\
* PeriodFormat.comma=,
* PeriodFormat.commandand=,i
* PeriodFormat.commaspaceand=, i
* PeriodFormat.commaspace=,
* PeriodFormat.spaceandspace=\ i
* PeriodFormat.regex.separator=%
* PeriodFormat.years.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.years.list=\ rok%\ lata%\ lat
* PeriodFormat.months.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.months.list=\ miesi\u0105c%\ miesi\u0105ce%\ miesi\u0119cy
* PeriodFormat.weeks.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.weeks.list=\ tydzie\u0144%\ tygodnie%\ tygodni
* PeriodFormat.day=\ dzie\u0144
* PeriodFormat.days=\ dni
* PeriodFormat.hours.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.hours.list=\ godzina%\ godziny%\ godzin
* PeriodFormat.minutes.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.minutes.list=\ minuta%\ minuty%\ minut
* PeriodFormat.seconds.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.seconds.list=\ sekunda%\ sekundy%\ sekund
* PeriodFormat.milliseconds.regex=^1$%[0-9]*(?&lt;!1)[2-4]$%[0-9]*
* PeriodFormat.milliseconds.list=\ milisekunda%\ milisekundy%\ milisekund
* </pre>
* Each PeriodFormat.&lt;duration_field_type&gt;.regex property stands for an array of
* regular expressions and is followed by a property
* PeriodFormat.&lt;duration_field_type&gt;.list holding an array of suffixes.
* PeriodFormat.regex.separator is used for splitting. See
* {@link PeriodFormatterBuilder#appendSuffix(String[], String[])} for details.
* <p>
* Available languages are English, Danish, Dutch, French, German, Japanese,
* Polish, Portuguese and Spanish.
*
* @param locale locale
* @return the formatter, not null
*/
public static PeriodFormatter wordBased(Locale locale) {
PeriodFormatter pf = FORMATTERS.get(locale);
if (pf == null) {
DynamicWordBased dynamic = new DynamicWordBased(buildWordBased(locale));
pf = new PeriodFormatter(dynamic, dynamic, locale);
PeriodFormatter existing = FORMATTERS.putIfAbsent(locale, pf);
if (existing != null) {
pf = existing;
}
}
return pf;
}
private static PeriodFormatter buildWordBased(Locale locale) {
ResourceBundle b = ResourceBundle.getBundle(BUNDLE_NAME, locale);
if (containsKey(b, "PeriodFormat.regex.separator")) {
return buildRegExFormatter(b, locale);
} else {
return buildNonRegExFormatter(b, locale);
}
}
private static PeriodFormatter buildRegExFormatter(ResourceBundle b, Locale locale) {
String[] variants = retrieveVariants(b);
String regExSeparator = b.getString("PeriodFormat.regex.separator");
PeriodFormatterBuilder builder = new PeriodFormatterBuilder();
builder.appendYears();
if (containsKey(b, "PeriodFormat.years.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.years.regex").split(regExSeparator),
b.getString("PeriodFormat.years.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.year"), b.getString("PeriodFormat.years"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendMonths();
if (containsKey(b, "PeriodFormat.months.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.months.regex").split(regExSeparator),
b.getString("PeriodFormat.months.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.month"), b.getString("PeriodFormat.months"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendWeeks();
if (containsKey(b, "PeriodFormat.weeks.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.weeks.regex").split(regExSeparator),
b.getString("PeriodFormat.weeks.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.week"), b.getString("PeriodFormat.weeks"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendDays();
if (containsKey(b, "PeriodFormat.days.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.days.regex").split(regExSeparator),
b.getString("PeriodFormat.days.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.day"), b.getString("PeriodFormat.days"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendHours();
if (containsKey(b, "PeriodFormat.hours.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.hours.regex").split(regExSeparator),
b.getString("PeriodFormat.hours.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.hour"), b.getString("PeriodFormat.hours"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendMinutes();
if (containsKey(b, "PeriodFormat.minutes.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.minutes.regex").split(regExSeparator),
b.getString("PeriodFormat.minutes.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.minute"), b.getString("PeriodFormat.minutes"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendSeconds();
if (containsKey(b, "PeriodFormat.seconds.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.seconds.regex").split(regExSeparator),
b.getString("PeriodFormat.seconds.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.second"), b.getString("PeriodFormat.seconds"));
}
builder.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants);
builder.appendMillis();
if (containsKey(b, "PeriodFormat.milliseconds.regex")) {
builder.appendSuffix(
b.getString("PeriodFormat.milliseconds.regex").split(regExSeparator),
b.getString("PeriodFormat.milliseconds.list").split(regExSeparator));
} else {
builder.appendSuffix(b.getString("PeriodFormat.millisecond"), b.getString("PeriodFormat.milliseconds"));
}
return builder.toFormatter().withLocale(locale);
}
private static PeriodFormatter buildNonRegExFormatter(ResourceBundle b, Locale locale) {
String[] variants = retrieveVariants(b);
return new PeriodFormatterBuilder()
.appendYears()
.appendSuffix(b.getString("PeriodFormat.year"), b.getString("PeriodFormat.years"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendMonths()
.appendSuffix(b.getString("PeriodFormat.month"), b.getString("PeriodFormat.months"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendWeeks()
.appendSuffix(b.getString("PeriodFormat.week"), b.getString("PeriodFormat.weeks"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendDays()
.appendSuffix(b.getString("PeriodFormat.day"), b.getString("PeriodFormat.days"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendHours()
.appendSuffix(b.getString("PeriodFormat.hour"), b.getString("PeriodFormat.hours"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendMinutes()
.appendSuffix(b.getString("PeriodFormat.minute"), b.getString("PeriodFormat.minutes"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendSeconds()
.appendSuffix(b.getString("PeriodFormat.second"), b.getString("PeriodFormat.seconds"))
.appendSeparator(b.getString("PeriodFormat.commaspace"), b.getString("PeriodFormat.spaceandspace"), variants)
.appendMillis()
.appendSuffix(b.getString("PeriodFormat.millisecond"), b.getString("PeriodFormat.milliseconds"))
.toFormatter()
.withLocale(locale);
}
private static String[] retrieveVariants(ResourceBundle b) {
return new String[]{b.getString("PeriodFormat.space"), b.getString("PeriodFormat.comma"),
b.getString("PeriodFormat.commandand"), b.getString("PeriodFormat.commaspaceand")};
}
// simulate ResourceBundle.containsKey()
private static boolean containsKey(ResourceBundle bundle, String key) {
for (Enumeration<String> en = bundle.getKeys(); en.hasMoreElements(); ) {
if (en.nextElement().equals(key)) {
return true;
}
}
return false;
}
/**
* Printer/parser that reacts to the locale and changes the word-based
* pattern if necessary.
*/
static class DynamicWordBased implements PeriodPrinter, PeriodParser {
/**
* The formatter with the locale selected at construction time.
*/
private final PeriodFormatter iFormatter;
DynamicWordBased(PeriodFormatter formatter) {
iFormatter = formatter;
}
@Override
public int countFieldsToPrint(Period period, int stopAt, Locale locale) {
return getPrinter(locale).countFieldsToPrint(period, stopAt, locale);
}
@Override
public int calculatePrintedLength(Period period, Locale locale) {
return getPrinter(locale).calculatePrintedLength(period, locale);
}
@Override
public void printTo(StringBuilder buf, Period period, Locale locale) {
getPrinter(locale).printTo(buf, period, locale);
}
@Override
public void printTo(Writer out, Period period, Locale locale) throws IOException {
getPrinter(locale).printTo(out, period, locale);
}
@Override
public int parseInto(PeriodAmount period, String periodStr, int position, Locale locale) {
return getParser(locale).parseInto(period, periodStr, position, locale);
}
private PeriodParser getParser(Locale locale) {
if (locale != null && !locale.equals(iFormatter.getLocale())) {
return wordBased(locale).getParser();
}
return iFormatter.getParser();
}
private PeriodPrinter getPrinter(Locale locale) {
if (locale != null && !locale.equals(iFormatter.getLocale())) {
return wordBased(locale).getPrinter();
}
return iFormatter.getPrinter();
}
}
}

View file

@ -0,0 +1,257 @@
package org.xbib.time.format;
import java.io.IOException;
import java.io.Writer;
import java.time.Period;
import java.util.Locale;
/**
* Controls the printing and parsing of a time period to and from a string.
* <p>
* This class is the main API for printing and parsing used by most applications.
* Instances of this class are created via one of three factory classes:
* <ul>
* <li>{@link PeriodFormat} - formats by pattern and style</li>
* <li>{@link ISOPeriodFormat} - ISO8601 formats</li>
* </ul>
* <p>
* An instance of this class holds a reference internally to one printer and
* one parser. It is possible that one of these may be null, in which case the
* formatter cannot print/parse. This can be checked via the {@link #isPrinter()}
* and {@link #isParser()} methods.
* <p>
* The underlying printer/parser can be altered to behave exactly as required
* by using a decorator modifier:
* <ul>
* <li>{@link #withLocale(Locale)} - returns a new formatter that uses the specified locale</li>
* </ul>
* This returns a new formatter (instances of this class are immutable).
* <p>
* The main methods of the class are the <code>printXxx</code> and
* <code>parseXxx</code> methods. These are used as follows:
* <pre>
* // print using the default locale
* String periodStr = formatter.print(period);
* // print using the French locale
* String periodStr = formatter.withLocale(Locale.FRENCH).print(period);
*
* // parse using the French locale
* Period date = formatter.withLocale(Locale.FRENCH).parsePeriod(str);
* </pre>
*/
public class PeriodFormatter {
/**
* The internal printer used to output the datetime.
*/
private final PeriodPrinter iPrinter;
/**
* The internal parser used to output the datetime.
*/
private final PeriodParser iParser;
/**
* The locale to use for printing and parsing.
*/
private final Locale iLocale;
/**
* Creates a new formatter, however you will normally use the factory
* or the builder.
*
* @param printer the internal printer, null if cannot print
* @param parser the internal parser, null if cannot parse
*/
public PeriodFormatter(PeriodPrinter printer, PeriodParser parser) {
super();
iPrinter = printer;
iParser = parser;
iLocale = null;
}
/**
* Constructor.
*
* @param printer the internal printer, null if cannot print
* @param parser the internal parser, null if cannot parse
* @param locale the locale to use
*/
PeriodFormatter(PeriodPrinter printer, PeriodParser parser, Locale locale) {
super();
iPrinter = printer;
iParser = parser;
iLocale = locale;
}
/**
* Is this formatter capable of printing.
*
* @return true if this is a printer
*/
public boolean isPrinter() {
return (iPrinter != null);
}
/**
* Gets the internal printer object that performs the real printing work.
*
* @return the internal printer
*/
public PeriodPrinter getPrinter() {
return iPrinter;
}
/**
* Is this formatter capable of parsing.
*
* @return true if this is a parser
*/
public boolean isParser() {
return (iParser != null);
}
/**
* Gets the internal parser object that performs the real parsing work.
*
* @return the internal parser
*/
public PeriodParser getParser() {
return iParser;
}
/**
* Returns a new formatter with a different locale that will be used
* for printing and parsing.
* <p>
* A PeriodFormatter is immutable, so a new instance is returned,
* and the original is unaltered and still usable.
* <p>
* A null locale indicates that no specific locale override is in use.
*
* @param locale the locale to use
* @return the new formatter
*/
public PeriodFormatter withLocale(Locale locale) {
if (locale == getLocale() || (locale != null && locale.equals(getLocale()))) {
return this;
}
return new PeriodFormatter(iPrinter, iParser, locale);
}
/**
* Gets the locale that will be used for printing and parsing.
* <p>
* A null locale indicates that no specific locale override is in use.
*
* @return the locale to use
*/
public Locale getLocale() {
return iLocale;
}
/**
* Prints a Period to a StringBuilder.
*
* @param buf the formatted period is appended to this buffer
* @param period the period to format, not null
*/
public void printTo(StringBuilder buf, Period period) {
checkPrinter();
checkPeriod(period);
getPrinter().printTo(buf, period, iLocale);
}
/**
* Prints a Period to a Writer.
*
* @param out the formatted period is written out
* @param period the period to format, not null
* @throws IOException if method fails
*/
public void printTo(Writer out, Period period) throws IOException {
checkPrinter();
checkPeriod(period);
getPrinter().printTo(out, period, iLocale);
}
/**
* Prints a Period to a new String.
*
* @param period the period to format, not null
* @return the printed result
*/
public String print(Period period) {
checkPrinter();
checkPeriod(period);
PeriodPrinter printer = getPrinter();
StringBuilder buf = new StringBuilder(printer.calculatePrintedLength(period, iLocale));
printer.printTo(buf, period, iLocale);
return buf.toString();
}
/**
* Parses a period from the given text, at the given position, saving the
* result into the fields of the given ReadWritablePeriod. If the parse
* succeeds, the return value is the new text position. Note that the parse
* may succeed without fully reading the text.
* The parse type of the formatter is not used by this method.
* If it fails, the return value is negative, but the period may still be
* modified. To determine the position where the parse failed, apply the
* one's complement operator (~) on the return value.
*
* @param period a period that will be modified
* @param text text to parse
* @param position position to start parsing from
* @return new position, if negative, parse failed. Apply complement
* operator (~) to get position of failure
* @throws IllegalArgumentException if any field is out of range
*/
public int parseInto(PeriodAmount period, String text, int position) {
checkParser();
checkPeriodAmount(period);
return getParser().parseInto(period, text, position, iLocale);
}
/**
* Checks whether parsing is supported.
*
* @throws UnsupportedOperationException if parsing is not supported
*/
private void checkParser() {
if (iParser == null) {
throw new UnsupportedOperationException("Parsing not supported");
}
}
/**
* Checks whether printing is supported.
*
* @throws UnsupportedOperationException if printing is not supported
*/
private void checkPrinter() {
if (iPrinter == null) {
throw new UnsupportedOperationException("Printing not supported");
}
}
/**
* Checks whether the period is non-null.
*
* @throws IllegalArgumentException if the period is null
*/
private void checkPeriod(Period period) {
if (period == null) {
throw new IllegalArgumentException("Period must not be null");
}
}
/**
* Checks whether the period amount is non-null.
*
* @throws IllegalArgumentException if the period amount is null
*/
private void checkPeriodAmount(PeriodAmount period) {
if (period == null) {
throw new IllegalArgumentException("PeriodAmount must not be null");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
package org.xbib.time.format;
import java.util.Locale;
/**
* Internal interface for parsing textual representations of time periods.
* Application users will rarely use this class directly. Instead, you
* will use one of the factory classes to create a {@link PeriodFormatter}.
* The factory classes are {@link PeriodFormat} and {@link ISOPeriodFormat}.
*/
public interface PeriodParser {
/**
* Parses a period from the given text, at the given position, saving the
* result into the given PeriodAmount. If the parse
* succeeds, the return value is the new text position. Note that the parse
* may succeed without fully reading the text.
* If it fails, the return value is negative, but the period may still be
* modified. To determine the position where the parse failed, apply the
* one's complement operator (~) on the return value.
*
* @param amount the period amount
* @param periodStr text to parse
* @param position position to start parsing from
* @param locale the locale to use for parsing
* @return new position, if negative, parse failed. Apply complement
* operator (~) to get position of failure
* @throws IllegalArgumentException if any field is out of range
*/
int parseInto(PeriodAmount amount, String periodStr, int position, Locale locale);
}

View file

@ -0,0 +1,55 @@
package org.xbib.time.format;
import java.io.IOException;
import java.io.Writer;
import java.time.Period;
import java.util.Locale;
/**
* Internal interface for printing textual representations of time periods.
* Application users will rarely use this class directly. Instead, you
* will use one of the factory classes to create a {@link PeriodFormatter}.
* The factory classes are {@link PeriodFormat} and{@link ISOPeriodFormat}.
*/
public interface PeriodPrinter {
/**
* Returns the exact number of characters produced for the given period.
*
* @param period the period to use
* @param locale the locale to use
* @return the estimated length
*/
int calculatePrintedLength(Period period, Locale locale);
/**
* Returns the amount of fields from the given period that this printer
* will print.
*
* @param period the period to use
* @param stopAt stop counting at this value, enter a number &ge; 256 to count all
* @param locale the locale to use
* @return amount of fields printed
*/
int countFieldsToPrint(Period period, int stopAt, Locale locale);
/**
* Prints a ReadablePeriod to a StringBuilder.
*
* @param buf the formatted period is appended to this buffer
* @param period the period to format
* @param locale the locale to use
*/
void printTo(StringBuilder buf, Period period, Locale locale);
/**
* Prints a ReadablePeriod to a Writer.
*
* @param out the formatted period is written out
* @param period the period to format
* @param locale the locale to use
* @throws IOException exception
*/
void printTo(Writer out, Period period, Locale locale) throws IOException;
}

View file

@ -0,0 +1,4 @@
/**
* Classes for time formatting.
*/
package org.xbib.time.format;

View file

@ -0,0 +1,18 @@
package org.xbib.time.pretty;
import java.util.Locale;
/**
* An object that behaves differently for various {@link Locale} settings.
*
* @param <T> parameter type
*/
public interface LocaleAware<T> {
/**
* Set the {@link Locale} for which this instance should behave in.
* @param locale locale
* @return the type
*/
T setLocale(Locale locale);
}

View file

@ -0,0 +1,468 @@
package org.xbib.time.pretty;
import org.xbib.time.pretty.i18n.ResourcesTimeFormat;
import org.xbib.time.pretty.i18n.ResourcesTimeUnit;
import org.xbib.time.pretty.units.Century;
import org.xbib.time.pretty.units.Day;
import org.xbib.time.pretty.units.Decade;
import org.xbib.time.pretty.units.Hour;
import org.xbib.time.pretty.units.JustNow;
import org.xbib.time.pretty.units.Millennium;
import org.xbib.time.pretty.units.Millisecond;
import org.xbib.time.pretty.units.Minute;
import org.xbib.time.pretty.units.Month;
import org.xbib.time.pretty.units.Second;
import org.xbib.time.pretty.units.TimeUnitComparator;
import org.xbib.time.pretty.units.Week;
import org.xbib.time.pretty.units.Year;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* A utility for creating social-networking style timestamps. (e.g. "just now", "moments ago", "3 days ago",
* "within 2 months")
* <p>
* <b>Usage:</b>
* <p>
* <code>
* PrettyTime t = new PrettyTime();
* String timestamp = t.format(LocalDateTime.now());
* //result: moments from now
* </code>
*/
public class PrettyTime {
/**
* The reference timestamp.
* If the timestamp formatted is before the reference timestamp, the format command will produce a String that is in the
* past tense. If the timestamp formatted is after the reference timestamp, the format command will produce a string
* thatis in the future tense.
*/
private LocalDateTime localDateTime;
private Locale locale;
private Map<TimeUnit, TimeFormat> units = new LinkedHashMap<>();
public PrettyTime() {
this(LocalDateTime.now());
}
public PrettyTime(long l) {
this(LocalDateTime.ofInstant(Instant.ofEpochMilli(l), ZoneId.systemDefault()));
}
/**
* Accept a {@link LocalDateTime} instant to represent the point of reference for comparison.
* This may be changed by theuser, after construction.
* <p>
* See {@code PrettyTime.setReference(LocalDateTime timestamp)}.
*
* @param localDateTime reference date time
*/
public PrettyTime(LocalDateTime localDateTime) {
this.localDateTime = localDateTime;
setLocale(Locale.getDefault());
initTimeUnits();
}
/**
* Construct a new instance using the given {@link Locale} instead of the system default.
* @param locale locale
*/
public PrettyTime(final Locale locale) {
setLocale(locale);
initTimeUnits();
this.localDateTime = LocalDateTime.now();
}
/**
* Accept a {@link LocalDateTime} timestamp to represent the point of reference for comparison.
* This may be changed by the user, after construction. Use the given {@link Locale}
* instead of the system default.
* <p>
* See {@code PrettyTime.setReference(LocalDateTime timestamp)}.
* @param localDateTime timestamp
* @param locale locale
*/
public PrettyTime(final LocalDateTime localDateTime, final Locale locale) {
setLocale(locale);
initTimeUnits();
this.localDateTime = localDateTime;
}
public PrettyTime(long l, final Locale locale) {
setLocale(locale);
initTimeUnits();
this.localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(l), ZoneId.systemDefault());
}
/**
* Calculate the approximate duration.
* @param then time instance
* @return time unit quantity
*/
public TimeUnitQuantity approximateDuration(LocalDateTime then) {
if (then == null) {
then = LocalDateTime.now();
}
long difference = ChronoUnit.MILLIS.between(localDateTime, then);
return calculateDuration(difference);
}
public TimeUnitQuantity calculateDuration(final long difference) {
long absoluteDifference = Math.abs(difference);
List<TimeUnit> units = new ArrayList<>();
units.addAll(getUnits());
TimeUnitQuantity result = new TimeUnitQuantity();
for (int i = 0; i < units.size(); i++) {
TimeUnit unit = units.get(i);
long millisPerUnit = Math.abs(unit.getMillisPerUnit());
long quantity = Math.abs(unit.getMaxQuantity());
boolean isLastUnit = (i == units.size() - 1);
if ((quantity == 0L) && !isLastUnit) {
quantity = units.get(i + 1).getMillisPerUnit() / unit.getMillisPerUnit();
}
// does our unit encompass the time duration?
if ((millisPerUnit * quantity > absoluteDifference) || isLastUnit) {
result.setUnit(unit);
if (millisPerUnit > absoluteDifference) {
// we are rounding up: get 1 or -1 for past or future
result.setQuantity(difference < 0L ? -1L : 1L);
} else {
result.setQuantity(difference / millisPerUnit);
}
result.setDelta(difference - result.getQuantity() * millisPerUnit);
break;
}
}
return result;
}
/**
* Calculate to the precision of the smallest provided {@link TimeUnit}, the exact duration represented by the
* difference between the reference timestamp.
* <p>
* <b>Note</b>: Precision may be lost if no supplied {@link TimeUnit} is granular enough to represent one
* millisecond
*
* @param then The date to be compared against the reference timestamp, or <i>now</i> if no reference timestamp was
* provided
* @return A sorted {@link List} of {@link TimeUnitQuantity} objects, from largest to smallest. Each element in the list
* represents the approximate duration (number of times) that {@link TimeUnit} to fit into the previous
* element's delta. The first element is the largest {@link TimeUnit} to fit within the total difference
* between compared dates.
*/
public List<TimeUnitQuantity> calculatePreciseDuration(LocalDateTime then) {
if (then == null) {
then = LocalDateTime.now();
}
List<TimeUnitQuantity> result = new ArrayList<>();
long difference = ChronoUnit.MILLIS.between(localDateTime, then);
TimeUnitQuantity timeUnitQuantity = calculateDuration(difference);
result.add(timeUnitQuantity);
while (timeUnitQuantity.getDelta() != 0L) {
timeUnitQuantity = calculateDuration(timeUnitQuantity.getDelta());
result.add(timeUnitQuantity);
}
return result;
}
public String format(long then) {
return format(approximateDuration(LocalDateTime.ofInstant(Instant.ofEpochMilli(then), ZoneId.systemDefault())));
}
/**
* Format the given {@link LocalDateTime} object. This method applies the {@code PrettyTime.approximateDuration(date)}
* method
* to perform its calculation. If {@code then} is null, it will default to {@code new Date()}; also decorate for
* past/future tense.
*
* @param then the {@link LocalDateTime} to be formatted
* @return A formatted string representing {@code then}
*/
public String format(LocalDateTime then) {
if (then == null) {
then = LocalDateTime.now();
}
return format(approximateDuration(then));
}
/**
* Format the given {@link LocalDateTime} object. This method applies the {@code PrettyTime.approximateDuration(date)}
* method
* to perform its calculation. If {@code then} is null, it will default to {@code new Date()}; also decorate for
* past/future tense. Rounding rules are ignored.
*
* @param then the {@link LocalDateTime} to be formatted
* @return A formatted string representing {@code then}
*/
public String formatUnrounded(final LocalDateTime then) {
return formatUnrounded(approximateDuration(then));
}
/**
* Format the given {@link TimeUnitQuantity} object, using the {@link TimeFormat} specified by the {@link TimeUnit}
* contained
* within; also decorate for past/future tense.
*
* @param timeUnitQuantity the {@link TimeUnitQuantity} to be formatted
* @return A formatted string representing {@code duration}
*/
public String format(final TimeUnitQuantity timeUnitQuantity) {
if (timeUnitQuantity == null) {
return format(LocalDateTime.now());
}
TimeFormat format = getFormat(timeUnitQuantity.getUnit());
String time = format.format(timeUnitQuantity);
return format.decorate(timeUnitQuantity, time);
}
/**
* Format the given {@link TimeUnitQuantity} object, using the {@link TimeFormat} specified by the {@link TimeUnit}
* contained
* within; also decorate for past/future tense. Rounding rules are ignored.
*
* @param timeUnitQuantity the {@link TimeUnitQuantity} to be formatted
* @return A formatted string representing {@code duration}
*/
public String formatUnrounded(final TimeUnitQuantity timeUnitQuantity) {
if (timeUnitQuantity == null) {
throw new IllegalArgumentException("Duration to format must not be null.");
}
TimeFormat format = getFormat(timeUnitQuantity.getUnit());
String time = format.formatUnrounded(timeUnitQuantity);
return format.decorateUnrounded(timeUnitQuantity, time);
}
/**
* Format the given {@link TimeUnitQuantity} objects, using the {@link TimeFormat} specified by the {@link TimeUnit}
* contained within. Rounds only the last {@link TimeUnitQuantity} object.
*
* @param timeUnitQuantities the {@link TimeUnitQuantity}s to be formatted
* @return A list of formatted strings representing {@code durations}
*/
public String format(final List<TimeUnitQuantity> timeUnitQuantities) {
if (timeUnitQuantities == null) {
throw new IllegalArgumentException("Duration list must not be null.");
}
String result = null;
StringBuilder builder = new StringBuilder();
TimeUnitQuantity timeUnitQuantity = null;
TimeFormat format = null;
for (int i = 0; i < timeUnitQuantities.size(); i++) {
timeUnitQuantity = timeUnitQuantities.get(i);
format = getFormat(timeUnitQuantity.getUnit());
boolean isLast = (i == timeUnitQuantities.size() - 1);
if (!isLast) {
builder.append(format.formatUnrounded(timeUnitQuantity));
builder.append(" ");
} else {
builder.append(format.format(timeUnitQuantity));
}
}
if (format != null) {
result = format.decorateUnrounded(timeUnitQuantity, builder.toString());
}
return result;
}
/**
* Given a date, returns a non-relative format string for the
* approximate duration of the difference between the date and now.
*
* @param date the date to be formatted
* @return A formatted string of the approximate duration
*/
public String formatApproximateDuration(LocalDateTime date) {
TimeUnitQuantity timeUnitQuantity = approximateDuration(date);
return formatDuration(timeUnitQuantity);
}
/**
* Given a duration, returns a non-relative format string.
*
* @param timeUnitQuantity the duration to be formatted
* @return A formatted string of the duration
*/
public String formatDuration(TimeUnitQuantity timeUnitQuantity) {
TimeFormat timeFormat = getFormat(timeUnitQuantity.getUnit());
return timeFormat.format(timeUnitQuantity);
}
/**
* Get the registered {@link TimeFormat} for the given {@link TimeUnit} or null if none exists.
* @param unit time unit
* @return time format
*/
public TimeFormat getFormat(TimeUnit unit) {
if (unit == null) {
throw new IllegalArgumentException("Time unit must not be null.");
}
return units.get(unit);
}
public PrettyTime setReference(LocalDateTime localDateTime) {
this.localDateTime = localDateTime;
return this;
}
/**
* Get a {@link List} of the current configured {@link TimeUnit} instances in calculations.
*
* @return list
*/
public List<TimeUnit> getUnits() {
List<TimeUnit> result = new ArrayList<>(units.keySet());
Collections.sort(result, new TimeUnitComparator());
return Collections.unmodifiableList(result);
}
/**
* Get the registered {@link TimeUnit} for the given {@link TimeUnit} type or null if none exists.
* @param unitType unit type
* @param <U> time unit type
* @return unit
*/
@SuppressWarnings("unchecked")
public <U extends TimeUnit> U getUnit(final Class<U> unitType) {
if (unitType == null) {
throw new IllegalArgumentException("Unit type to get must not be null.");
}
for (TimeUnit unit : units.keySet()) {
if (unitType.isAssignableFrom(unit.getClass())) {
return (U) unit;
}
}
return null;
}
/**
* Register the given {@link TimeUnit} and corresponding {@link TimeFormat} instance to be used in calculations. If
* an entry already exists for the given {@link TimeUnit}, its format will be overwritten with the given
* {@link TimeFormat}.
* @param unit unit
* @param format format
* @return this object
*/
public PrettyTime registerUnit(final TimeUnit unit, TimeFormat format) {
if (unit == null) {
throw new IllegalArgumentException("Unit to register must not be null.");
}
if (format == null) {
throw new IllegalArgumentException("Format to register must not be null.");
}
units.put(unit, format);
if (unit instanceof LocaleAware) {
((LocaleAware<?>) unit).setLocale(locale);
}
if (format instanceof LocaleAware) {
((LocaleAware<?>) format).setLocale(locale);
}
return this;
}
/**
* Removes the mapping for the given {@link TimeUnit} type. This effectively de-registers the unit so it will not
* be used in formatting. Returns the {@link TimeFormat} that was registered for the given {@link TimeUnit} type, or
* null if no unit of the given type was registered.
* @param unitType unit type
* @param <U> time unit type
* @return time unit
*/
public <U extends TimeUnit> TimeFormat removeUnit(final Class<U> unitType) {
if (unitType == null) {
throw new IllegalArgumentException("Unit type to remove must not be null.");
}
for (TimeUnit unit : units.keySet()) {
if (unitType.isAssignableFrom(unit.getClass())) {
return units.remove(unit);
}
}
return null;
}
/**
* Removes the mapping for the given {@link TimeUnit}. This effectively de-registers the unit so it will not be
* used in formatting. Returns the {@link TimeFormat} that was registered for the given {@link TimeUnit},
* or null if no such unit was registered.
* @param unit time unit
* @return time format
*/
public TimeFormat removeUnit(final TimeUnit unit) {
if (unit == null) {
throw new IllegalArgumentException("Unit to remove must not be null.");
}
return units.remove(unit);
}
/**
* Get the currently configured {@link Locale} for this {@link PrettyTime} object.
* @return locale
*/
public Locale getLocale() {
return locale;
}
/**
* Set the the {@link Locale} for this {@link PrettyTime} object. This may be an expensive operation, since this
* operation calls {@link LocaleAware#setLocale(Locale)} for each {@link TimeUnit} in {@link #getUnits()}.
* @param locale locale
* @return this object
*/
public PrettyTime setLocale(final Locale locale) {
this.locale = locale;
units.keySet().stream().filter(unit -> unit instanceof LocaleAware)
.forEach(unit -> ((LocaleAware<?>) unit).setLocale(locale));
units.values().stream().filter(format -> format instanceof LocaleAware)
.forEach(format -> ((LocaleAware<?>) format).setLocale(locale));
return this;
}
@Override
public String toString() {
return "PrettyTime [date=" + localDateTime + ", locale=" + locale + "]";
}
/**
* Remove all registered {@link TimeUnit} instances.
*
* @return The removed {@link TimeUnit} instances.
*/
public List<TimeUnit> clearUnits() {
List<TimeUnit> result = getUnits();
units.clear();
return result;
}
private void initTimeUnits() {
addUnit(new JustNow());
addUnit(new Millisecond());
addUnit(new Second());
addUnit(new Minute());
addUnit(new Hour());
addUnit(new Day());
addUnit(new Week());
addUnit(new Month());
addUnit(new Year());
addUnit(new Decade());
addUnit(new Century());
addUnit(new Millennium());
}
private void addUnit(ResourcesTimeUnit unit) {
registerUnit(unit, new ResourcesTimeFormat(unit));
}
}

View file

@ -0,0 +1,189 @@
package org.xbib.time.pretty;
/**
* Represents a simple method of formatting a specific {@link TimeUnitQuantity} of time.
*/
public class SimpleTimeFormat implements TimeFormat {
private static final String SIGN = "%s";
private static final String QUANTITY = "%n";
private static final String UNIT = "%u";
private static final String NEGATIVE = "-";
private String singularName = "";
private String pluralName = "";
private String futureSingularName = "";
private String futurePluralName = "";
private String pastSingularName = "";
private String pastPluralName = "";
private String pattern = "";
private String futurePrefix = "";
private String futureSuffix = "";
private String pastPrefix = "";
private String pastSuffix = "";
private int roundingTolerance = 50;
@Override
public String format(final TimeUnitQuantity timeUnitQuantity) {
return format(timeUnitQuantity, true);
}
@Override
public String formatUnrounded(TimeUnitQuantity timeUnitQuantity) {
return format(timeUnitQuantity, false);
}
@Override
public String decorate(TimeUnitQuantity timeUnitQuantity, String time) {
StringBuilder result = new StringBuilder();
if (timeUnitQuantity.isInPast()) {
result.append(pastPrefix).append(" ").append(time).append(" ").append(pastSuffix);
} else {
result.append(futurePrefix).append(" ").append(time).append(" ").append(futureSuffix);
}
return result.toString().replaceAll("\\s+", " ").trim();
}
@Override
public String decorateUnrounded(TimeUnitQuantity timeUnitQuantity, String time) {
// This format does not need to know about rounding during decoration.
return decorate(timeUnitQuantity, time);
}
private String format(final TimeUnitQuantity timeUnitQuantity, final boolean round) {
String sign = getSign(timeUnitQuantity);
String unit = getGramaticallyCorrectName(timeUnitQuantity, round);
long quantity = getQuantity(timeUnitQuantity, round);
return applyPattern(sign, unit, quantity);
}
private String applyPattern(final String sign, final String unit, final long quantity) {
String result = getPattern(quantity).replaceAll(SIGN, sign);
result = result.replaceAll(QUANTITY, String.valueOf(quantity));
result = result.replaceAll(UNIT, unit);
return result;
}
protected String getPattern(final long quantity) {
return pattern;
}
public String getPattern() {
return pattern;
}
/*
* Builder Setters
*/
public SimpleTimeFormat setPattern(final String pattern) {
this.pattern = pattern;
return this;
}
protected long getQuantity(TimeUnitQuantity timeUnitQuantity, boolean round) {
return Math.abs(round ? timeUnitQuantity.getQuantityRounded(roundingTolerance) : timeUnitQuantity.getQuantity());
}
protected String getGramaticallyCorrectName(final TimeUnitQuantity d, boolean round) {
String result = getSingularName(d);
if ((Math.abs(getQuantity(d, round)) == 0) || (Math.abs(getQuantity(d, round)) > 1)) {
result = getPluralName(d);
}
return result;
}
private String getSign(final TimeUnitQuantity d) {
if (d.getQuantity() < 0) {
return NEGATIVE;
}
return "";
}
private String getSingularName(TimeUnitQuantity timeUnitQuantity) {
if (timeUnitQuantity.isInFuture() && futureSingularName != null && futureSingularName.length() > 0) {
return futureSingularName;
} else if (timeUnitQuantity.isInPast() && pastSingularName != null && pastSingularName.length() > 0) {
return pastSingularName;
} else {
return singularName;
}
}
private String getPluralName(TimeUnitQuantity timeUnitQuantity) {
if (timeUnitQuantity.isInFuture() && futurePluralName != null && futureSingularName.length() > 0) {
return futurePluralName;
} else if (timeUnitQuantity.isInPast() && pastPluralName != null && pastSingularName.length() > 0) {
return pastPluralName;
} else {
return pluralName;
}
}
public SimpleTimeFormat setFuturePrefix(final String futurePrefix) {
this.futurePrefix = futurePrefix.trim();
return this;
}
public SimpleTimeFormat setFutureSuffix(final String futureSuffix) {
this.futureSuffix = futureSuffix.trim();
return this;
}
public SimpleTimeFormat setPastPrefix(final String pastPrefix) {
this.pastPrefix = pastPrefix.trim();
return this;
}
public SimpleTimeFormat setPastSuffix(final String pastSuffix) {
this.pastSuffix = pastSuffix.trim();
return this;
}
/**
* The percentage of the current {@link TimeUnit}.getMillisPerUnit() for which the quantity may be rounded up by
* one.
*
* @param roundingTolerance tolerance
* @return time format
*/
public SimpleTimeFormat setRoundingTolerance(final int roundingTolerance) {
this.roundingTolerance = roundingTolerance;
return this;
}
public SimpleTimeFormat setSingularName(String name) {
this.singularName = name;
return this;
}
public SimpleTimeFormat setPluralName(String pluralName) {
this.pluralName = pluralName;
return this;
}
public SimpleTimeFormat setFutureSingularName(String futureSingularName) {
this.futureSingularName = futureSingularName;
return this;
}
public SimpleTimeFormat setFuturePluralName(String futurePluralName) {
this.futurePluralName = futurePluralName;
return this;
}
public SimpleTimeFormat setPastSingularName(String pastSingularName) {
this.pastSingularName = pastSingularName;
return this;
}
public SimpleTimeFormat setPastPluralName(String pastPluralName) {
this.pastPluralName = pastPluralName;
return this;
}
@Override
public String toString() {
return "SimpleTimeFormat [pattern=" + pattern + ", futurePrefix=" + futurePrefix + ", futureSuffix="
+ futureSuffix + ", pastPrefix=" + pastPrefix + ", pastSuffix=" + pastSuffix + ", roundingTolerance="
+ roundingTolerance + "]";
}
}

View file

@ -0,0 +1,40 @@
package org.xbib.time.pretty;
/**
*/
public interface TimeFormat {
/**
* Given a populated {@link TimeUnitQuantity} object. Apply formatting (with rounding) and output the result.
*
* @param timeUnitQuantity The original {@link TimeUnitQuantity} instance from which the time string should be decorated.
* @return the formatted string
*/
String format(final TimeUnitQuantity timeUnitQuantity);
/**
* Given a populated {@link TimeUnitQuantity} object. Apply formatting (without rounding) and output the result.
*
* @param timeUnitQuantity The original {@link TimeUnitQuantity} instance from which the time string should be decorated.
* @return the formatted string
*/
String formatUnrounded(TimeUnitQuantity timeUnitQuantity);
/**
* Decorate with past or future prefix/suffix (with rounding).
*
* @param timeUnitQuantity The original {@link TimeUnitQuantity} instance from which the time string should be decorated.
* @param time The formatted time string.
* @return the formatted string
*/
String decorate(TimeUnitQuantity timeUnitQuantity, String time);
/**
* Decorate with past or future prefix/suffix (without rounding).
*
* @param timeUnitQuantity The original {@link TimeUnitQuantity} instance from which the time string should be decorated.
* @param time The formatted time string.
* @return the formatted string
*/
String decorateUnrounded(TimeUnitQuantity timeUnitQuantity, String time);
}

View file

@ -0,0 +1,16 @@
package org.xbib.time.pretty;
import org.xbib.time.pretty.i18n.ResourcesTimeFormat;
/**
* Produces time formats. Currently only to be used on Resource bundle implementations when used in
* {@link ResourcesTimeFormat} instances.
*/
public interface TimeFormatProvider {
/**
* Return the appropriate {@link TimeFormat} for the given {@link TimeUnit}.
* @param t time unit
* @return time format
*/
TimeFormat getFormatFor(TimeUnit t);
}

View file

@ -0,0 +1,32 @@
package org.xbib.time.pretty;
/**
* Defines a Unit of time (e.g. seconds, minutes, hours) and its conversion to milliseconds.
*/
public interface TimeUnit {
/**
* The number of milliseconds represented by each instance of this TimeUnit.
* Must be a positive number greater than zero.
* @return millis
*/
long getMillisPerUnit();
/**
* The maximum quantity of this Unit to be used as a threshold for the next
* largest Unit (e.g. if one <code>Second</code> represents 1000ms, and
* <code>Second</code> has a maxQuantity of 5, then if the difference
* between compared timestamps is larger than 5000ms, PrettyTime will move
* on to the next smallest TimeUnit for calculation; <code>Minute</code>, by
* default)
* <p>
* millisPerUnit * maxQuantity = maxAllowedMs
* <p>
* If maxQuantity is zero, it will be equal to the next highest
* <code>TimeUnit.getMillisPerUnit() /
* this.getMillisPerUnit()</code> or infinity if there are no greater
* TimeUnits.
* @return quantity
*/
long getMaxQuantity();
}

View file

@ -0,0 +1,56 @@
package org.xbib.time.pretty;
/**
* Represents a quantity of any given {@link TimeUnit}.
*/
public class TimeUnitQuantity {
private long quantity;
private long delta;
private TimeUnit unit;
public long getQuantity() {
return quantity;
}
public void setQuantity(final long quantity) {
this.quantity = quantity;
}
public TimeUnit getUnit() {
return unit;
}
public void setUnit(final TimeUnit unit) {
this.unit = unit;
}
public long getDelta() {
return delta;
}
public void setDelta(final long delta) {
this.delta = delta;
}
public boolean isInPast() {
return getQuantity() < 0;
}
public boolean isInFuture() {
return !isInPast();
}
public long getQuantityRounded(int tolerance) {
long quantity = Math.abs(getQuantity());
if (getDelta() != 0) {
double threshold = Math.abs(((double) getDelta() / (double) getUnit().getMillisPerUnit()) * 100);
if (threshold > tolerance) {
quantity = quantity + 1;
}
}
return quantity;
}
}

View file

@ -0,0 +1,102 @@
package org.xbib.time.pretty.i18n;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.Set;
/**
*
*/
public abstract class MapResourceBundle extends ResourceBundle {
private Map<String, Object> lookup;
public MapResourceBundle() {
}
public final Object handleGetObject(String key) {
if (lookup == null) {
loadLookup();
}
return lookup.get(key);
}
public Enumeration<String> getKeys() {
if (lookup == null) {
loadLookup();
}
ResourceBundle parent = this.parent;
return new ResourceBundleEnumeration(lookup.keySet(), parent != null ? parent.getKeys() : null);
}
protected Set<String> handleKeySet() {
if (lookup == null) {
loadLookup();
}
return lookup.keySet();
}
protected abstract Map<String, Object> getContents();
private synchronized void loadLookup() {
if (lookup != null) {
return;
}
Map<String, Object> contents = getContents();
Map<String, Object> temp = new HashMap<>();
for (Map.Entry<String, Object> entry : contents.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key == null || value == null) {
throw new NullPointerException();
}
temp.put(key, value);
}
lookup = temp;
}
private static class ResourceBundleEnumeration implements Enumeration<String> {
private Set<String> set;
private Iterator<String> iterator;
private Enumeration<String> enumeration;
private String next = null;
ResourceBundleEnumeration(Set<String> var1, Enumeration<String> var2) {
this.set = var1;
this.iterator = var1.iterator();
this.enumeration = var2;
}
@Override
public boolean hasMoreElements() {
if (next == null) {
if (iterator.hasNext()) {
next = iterator.next();
} else if (enumeration != null) {
while (next == null && enumeration.hasMoreElements()) {
next = enumeration.nextElement();
if (set.contains(next)) {
next = null;
}
}
}
}
return next != null;
}
@Override
public String nextElement() {
if (this.hasMoreElements()) {
String var1 = this.next;
this.next = null;
return var1;
} else {
throw new NoSuchElementException();
}
}
}
}

View file

@ -0,0 +1,111 @@
package org.xbib.time.pretty.i18n;
import java.util.HashMap;
import java.util.Map;
/**
*
*/
public class Resources extends MapResourceBundle {
private static final Map<String, Object> map = new HashMap<>();
static {
map.put("CenturyPattern", "%n %u");
map.put("CenturyFuturePrefix", "");
map.put("CenturyFutureSuffix", " from now");
map.put("CenturyPastPrefix", "");
map.put("CenturyPastSuffix", " ago");
map.put("CenturySingularName", "century");
map.put("CenturyPluralName", "centuries");
map.put("DayPattern", "%n %u");
map.put("DayFuturePrefix", "");
map.put("DayFutureSuffix", " from now");
map.put("DayPastPrefix", "");
map.put("DayPastSuffix", " ago");
map.put("DaySingularName", "day");
map.put("DayPluralName", "days");
map.put("DecadePattern", "%n %u");
map.put("DecadeFuturePrefix", "");
map.put("DecadeFutureSuffix", " from now");
map.put("DecadePastPrefix", "");
map.put("DecadePastSuffix", " ago");
map.put("DecadeSingularName", "decade");
map.put("DecadePluralName", "decades");
map.put("HourPattern", "%n %u");
map.put("HourFuturePrefix", "");
map.put("HourFutureSuffix", " from now");
map.put("HourPastPrefix", "");
map.put("HourPastSuffix", " ago");
map.put("HourSingularName", "hour");
map.put("HourPluralName", "hours");
map.put("JustNowPattern", "%u");
map.put("JustNowFuturePrefix", "");
map.put("JustNowFutureSuffix", "moments from now");
map.put("JustNowPastPrefix", "moments ago");
map.put("JustNowPastSuffix", "");
map.put("JustNowSingularName", "");
map.put("JustNowPluralName", "");
map.put("MillenniumPattern", "%n %u");
map.put("MillenniumFuturePrefix", "");
map.put("MillenniumFutureSuffix", " from now");
map.put("MillenniumPastPrefix", "");
map.put("MillenniumPastSuffix", " ago");
map.put("MillenniumSingularName", "millennium");
map.put("MillenniumPluralName", "millennia");
map.put("MillisecondPattern", "%n %u");
map.put("MillisecondFuturePrefix", "");
map.put("MillisecondFutureSuffix", " from now");
map.put("MillisecondPastPrefix", "");
map.put("MillisecondPastSuffix", " ago");
map.put("MillisecondSingularName", "millisecond");
map.put("MillisecondPluralName", "milliseconds");
map.put("MinutePattern", "%n %u");
map.put("MinuteFuturePrefix", "");
map.put("MinuteFutureSuffix", " from now");
map.put("MinutePastPrefix", "");
map.put("MinutePastSuffix", " ago");
map.put("MinuteSingularName", "minute");
map.put("MinutePluralName", "minutes");
map.put("MonthPattern", "%n %u");
map.put("MonthFuturePrefix", "");
map.put("MonthFutureSuffix", " from now");
map.put("MonthPastPrefix", "");
map.put("MonthPastSuffix", " ago");
map.put("MonthSingularName", "month");
map.put("MonthPluralName", "months");
map.put("SecondPattern", "%n %u");
map.put("SecondFuturePrefix", "");
map.put("SecondFutureSuffix", " from now");
map.put("SecondPastPrefix", "");
map.put("SecondPastSuffix", " ago");
map.put("SecondSingularName", "second");
map.put("SecondPluralName", "seconds");
map.put("WeekPattern", "%n %u");
map.put("WeekFuturePrefix", "");
map.put("WeekFutureSuffix", " from now");
map.put("WeekPastPrefix", "");
map.put("WeekPastSuffix", " ago");
map.put("WeekSingularName", "week");
map.put("WeekPluralName", "weeks");
map.put("YearPattern", "%n %u");
map.put("YearFuturePrefix", "");
map.put("YearFutureSuffix", " from now");
map.put("YearPastPrefix", "");
map.put("YearPastSuffix", " ago");
map.put("YearSingularName", "year");
map.put("YearPluralName", "years");
map.put("AbstractTimeUnitPattern", "");
map.put("AbstractTimeUnitFuturePrefix", "");
map.put("AbstractTimeUnitFutureSuffix", "");
map.put("AbstractTimeUnitPastPrefix", "");
map.put("AbstractTimeUnitPastSuffix", "");
map.put("AbstractTimeUnitSingularName", "");
map.put("AbstractTimeUnitPluralName", "");
}
@Override
public Map<String, Object> getContents() {
return map;
}
}

View file

@ -0,0 +1,85 @@
package org.xbib.time.pretty.i18n;
import org.xbib.time.pretty.LocaleAware;
import org.xbib.time.pretty.SimpleTimeFormat;
import org.xbib.time.pretty.TimeFormat;
import org.xbib.time.pretty.TimeFormatProvider;
import org.xbib.time.pretty.TimeUnitQuantity;
import java.util.Locale;
import java.util.ResourceBundle;
/**
* Represents a simple method of formatting a specific {@link TimeUnitQuantity} of time.
*/
public class ResourcesTimeFormat extends SimpleTimeFormat implements TimeFormat, LocaleAware<ResourcesTimeFormat> {
private final ResourcesTimeUnit unit;
private TimeFormat override;
public ResourcesTimeFormat(ResourcesTimeUnit unit) {
this.unit = unit;
}
@Override
public ResourcesTimeFormat setLocale(Locale locale) {
ResourceBundle bundle = ResourceBundle.getBundle(unit.getResourceBundleName(), locale);
if (bundle instanceof TimeFormatProvider) {
TimeFormat format = ((TimeFormatProvider) bundle).getFormatFor(unit);
if (format != null) {
this.override = format;
}
} else {
override = null;
}
if (override == null) {
setPattern(bundle.getString(unit.getResourceKeyPrefix() + "Pattern"));
setFuturePrefix(bundle.getString(unit.getResourceKeyPrefix() + "FuturePrefix"));
setFutureSuffix(bundle.getString(unit.getResourceKeyPrefix() + "FutureSuffix"));
setPastPrefix(bundle.getString(unit.getResourceKeyPrefix() + "PastPrefix"));
setPastSuffix(bundle.getString(unit.getResourceKeyPrefix() + "PastSuffix"));
setSingularName(bundle.getString(unit.getResourceKeyPrefix() + "SingularName"));
setPluralName(bundle.getString(unit.getResourceKeyPrefix() + "PluralName"));
String key = unit.getResourceKeyPrefix() + "FuturePluralName";
if (bundle.containsKey(key)) {
setFuturePluralName(bundle.getString(key));
}
key = unit.getResourceKeyPrefix() + "FutureSingularName";
if (bundle.containsKey(key)) {
setFutureSingularName((bundle.getString(key)));
}
key = unit.getResourceKeyPrefix() + "PastPluralName";
if (bundle.containsKey(key)) {
setPastPluralName((bundle.getString(key)));
}
key = unit.getResourceKeyPrefix() + "PastSingularName";
if (bundle.containsKey(key)) {
setPastSingularName((bundle.getString(key)));
}
}
return this;
}
@Override
public String decorate(TimeUnitQuantity timeUnitQuantity, String time) {
return override == null ? super.decorate(timeUnitQuantity, time) : override.decorate(timeUnitQuantity, time);
}
@Override
public String decorateUnrounded(TimeUnitQuantity timeUnitQuantity, String time) {
return override == null ? super.decorateUnrounded(timeUnitQuantity, time) :
override.decorateUnrounded(timeUnitQuantity, time);
}
@Override
public String format(TimeUnitQuantity timeUnitQuantity) {
return override == null ? super.format(timeUnitQuantity) : override.format(timeUnitQuantity);
}
@Override
public String formatUnrounded(TimeUnitQuantity timeUnitQuantity) {
return override == null ? super.formatUnrounded(timeUnitQuantity) : override.formatUnrounded(timeUnitQuantity);
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.time.pretty.i18n;
import org.xbib.time.pretty.TimeUnit;
/**
*
*/
public abstract class ResourcesTimeUnit implements TimeUnit {
private long maxQuantity = 0;
private long millisPerUnit = 1;
protected abstract String getResourceKeyPrefix();
protected String getResourceBundleName() {
return Resources.class.getName();
}
@Override
public long getMaxQuantity() {
return maxQuantity;
}
public void setMaxQuantity(long maxQuantity) {
this.maxQuantity = maxQuantity;
}
@Override
public long getMillisPerUnit() {
return millisPerUnit;
}
public void setMillisPerUnit(long millisPerUnit) {
this.millisPerUnit = millisPerUnit;
}
}

Some files were not shown because too many files have changed in this diff Show more