initial commit
This commit is contained in:
commit
2aead18a0f
199 changed files with 20184 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal 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
202
LICENSE.txt
Normal 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
79
build.gradle
Normal 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'
|
323
config/checkstyle/checkstyle.xml
Normal file
323
config/checkstyle/checkstyle.xml
Normal 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
8
gradle/ext.gradle
Normal 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
73
gradle/publish.gradle
Normal 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
41
gradle/sonarqube.gradle
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
1
settings.gradle
Normal file
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = 'time'
|
193
src/main/java/org/xbib/time/chronic/Chronic.java
Normal file
193
src/main/java/org/xbib/time/chronic/Chronic.java
Normal 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 => 3), and converting ordinal words to numeric
|
||||||
|
* ordinals (third => 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 => 3).
|
||||||
|
* @param text text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static String numericizeNumbers(String text) {
|
||||||
|
return Numerizer.numerize(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ordinal words to numeric ordinals (third => 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());
|
||||||
|
}
|
||||||
|
}
|
81
src/main/java/org/xbib/time/chronic/Options.java
Normal file
81
src/main/java/org/xbib/time/chronic/Options.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
src/main/java/org/xbib/time/chronic/Range.java
Normal file
40
src/main/java/org/xbib/time/chronic/Range.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
54
src/main/java/org/xbib/time/chronic/Span.java
Normal file
54
src/main/java/org/xbib/time/chronic/Span.java
Normal 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()) + ")";
|
||||||
|
}
|
||||||
|
}
|
35
src/main/java/org/xbib/time/chronic/Tick.java
Normal file
35
src/main/java/org/xbib/time/chronic/Tick.java
Normal 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 ? "?" : "");
|
||||||
|
}
|
||||||
|
}
|
97
src/main/java/org/xbib/time/chronic/Token.java
Normal file
97
src/main/java/org/xbib/time/chronic/Token.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
386
src/main/java/org/xbib/time/chronic/handlers/Handler.java
Normal file
386
src/main/java/org/xbib/time/chronic/handlers/Handler.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
14
src/main/java/org/xbib/time/chronic/handlers/IHandler.java
Normal file
14
src/main/java/org/xbib/time/chronic/handlers/IHandler.java
Normal 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);
|
||||||
|
}
|
25
src/main/java/org/xbib/time/chronic/handlers/MDHandler.java
Normal file
25
src/main/java/org/xbib/time/chronic/handlers/MDHandler.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/main/java/org/xbib/time/chronic/handlers/ORRHandler.java
Normal file
32
src/main/java/org/xbib/time/chronic/handlers/ORRHandler.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
src/main/java/org/xbib/time/chronic/handlers/PSRHandler.java
Normal file
23
src/main/java/org/xbib/time/chronic/handlers/PSRHandler.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
src/main/java/org/xbib/time/chronic/handlers/RGRHandler.java
Normal file
23
src/main/java/org/xbib/time/chronic/handlers/RGRHandler.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
19
src/main/java/org/xbib/time/chronic/handlers/RHandler.java
Normal file
19
src/main/java/org/xbib/time/chronic/handlers/RHandler.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
src/main/java/org/xbib/time/chronic/handlers/SRPHandler.java
Normal file
35
src/main/java/org/xbib/time/chronic/handlers/SRPHandler.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
29
src/main/java/org/xbib/time/chronic/handlers/TagPattern.java
Normal file
29
src/main/java/org/xbib/time/chronic/handlers/TagPattern.java
Normal 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 + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Classes for chronic handlers.
|
||||||
|
*/
|
||||||
|
package org.xbib.time.chronic.handlers;
|
194
src/main/java/org/xbib/time/chronic/numerizer/Numerizer.java
Normal file
194
src/main/java/org/xbib/time/chronic/numerizer/Numerizer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Classes for chronic numerizers.
|
||||||
|
*/
|
||||||
|
package org.xbib.time.chronic.numerizer;
|
4
src/main/java/org/xbib/time/chronic/package-info.java
Normal file
4
src/main/java/org/xbib/time/chronic/package-info.java
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Classes for time chronic.
|
||||||
|
*/
|
||||||
|
package org.xbib.time.chronic;
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
91
src/main/java/org/xbib/time/chronic/repeaters/Repeater.java
Normal file
91
src/main/java/org/xbib/time/chronic/repeaters/Repeater.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
223
src/main/java/org/xbib/time/chronic/repeaters/RepeaterTime.java
Normal file
223
src/main/java/org/xbib/time/chronic/repeaters/RepeaterTime.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Classes for chronic repeaters.
|
||||||
|
*/
|
||||||
|
package org.xbib.time.chronic.repeaters;
|
58
src/main/java/org/xbib/time/chronic/tags/Grabber.java
Normal file
58
src/main/java/org/xbib/time/chronic/tags/Grabber.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
48
src/main/java/org/xbib/time/chronic/tags/Ordinal.java
Normal file
48
src/main/java/org/xbib/time/chronic/tags/Ordinal.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
30
src/main/java/org/xbib/time/chronic/tags/OrdinalDay.java
Normal file
30
src/main/java/org/xbib/time/chronic/tags/OrdinalDay.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
61
src/main/java/org/xbib/time/chronic/tags/Pointer.java
Normal file
61
src/main/java/org/xbib/time/chronic/tags/Pointer.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
103
src/main/java/org/xbib/time/chronic/tags/Scalar.java
Normal file
103
src/main/java/org/xbib/time/chronic/tags/Scalar.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
32
src/main/java/org/xbib/time/chronic/tags/ScalarDay.java
Normal file
32
src/main/java/org/xbib/time/chronic/tags/ScalarDay.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
32
src/main/java/org/xbib/time/chronic/tags/ScalarMonth.java
Normal file
32
src/main/java/org/xbib/time/chronic/tags/ScalarMonth.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
37
src/main/java/org/xbib/time/chronic/tags/ScalarYear.java
Normal file
37
src/main/java/org/xbib/time/chronic/tags/ScalarYear.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
51
src/main/java/org/xbib/time/chronic/tags/Separator.java
Normal file
51
src/main/java/org/xbib/time/chronic/tags/Separator.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
36
src/main/java/org/xbib/time/chronic/tags/SeparatorAt.java
Normal file
36
src/main/java/org/xbib/time/chronic/tags/SeparatorAt.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
37
src/main/java/org/xbib/time/chronic/tags/SeparatorComma.java
Normal file
37
src/main/java/org/xbib/time/chronic/tags/SeparatorComma.java
Normal 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
src/main/java/org/xbib/time/chronic/tags/SeparatorIn.java
Normal file
36
src/main/java/org/xbib/time/chronic/tags/SeparatorIn.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
10
src/main/java/org/xbib/time/chronic/tags/StringTag.java
Normal file
10
src/main/java/org/xbib/time/chronic/tags/StringTag.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package org.xbib.time.chronic.tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class StringTag extends Tag<String> {
|
||||||
|
public StringTag(String type) {
|
||||||
|
super(type);
|
||||||
|
}
|
||||||
|
}
|
36
src/main/java/org/xbib/time/chronic/tags/Tag.java
Normal file
36
src/main/java/org/xbib/time/chronic/tags/Tag.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
46
src/main/java/org/xbib/time/chronic/tags/TimeZone.java
Normal file
46
src/main/java/org/xbib/time/chronic/tags/TimeZone.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Classes for chronic tags.
|
||||||
|
*/
|
||||||
|
package org.xbib.time.chronic.tags;
|
430
src/main/java/org/xbib/time/format/FormatUtils.java
Normal file
430
src/main/java/org/xbib/time/format/FormatUtils.java
Normal 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) + '"';
|
||||||
|
}
|
||||||
|
}
|
190
src/main/java/org/xbib/time/format/ISOPeriodFormat.java
Normal file
190
src/main/java/org/xbib/time/format/ISOPeriodFormat.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
41
src/main/java/org/xbib/time/format/PeriodAmount.java
Normal file
41
src/main/java/org/xbib/time/format/PeriodAmount.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
374
src/main/java/org/xbib/time/format/PeriodFormat.java
Normal file
374
src/main/java/org/xbib/time/format/PeriodFormat.java
Normal 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_<locale>.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]*(?<!1)[2-4]$%[0-9]*
|
||||||
|
* PeriodFormat.years.list=\ rok%\ lata%\ lat
|
||||||
|
* PeriodFormat.months.regex=^1$%[0-9]*(?<!1)[2-4]$%[0-9]*
|
||||||
|
* PeriodFormat.months.list=\ miesi\u0105c%\ miesi\u0105ce%\ miesi\u0119cy
|
||||||
|
* PeriodFormat.weeks.regex=^1$%[0-9]*(?<!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]*(?<!1)[2-4]$%[0-9]*
|
||||||
|
* PeriodFormat.hours.list=\ godzina%\ godziny%\ godzin
|
||||||
|
* PeriodFormat.minutes.regex=^1$%[0-9]*(?<!1)[2-4]$%[0-9]*
|
||||||
|
* PeriodFormat.minutes.list=\ minuta%\ minuty%\ minut
|
||||||
|
* PeriodFormat.seconds.regex=^1$%[0-9]*(?<!1)[2-4]$%[0-9]*
|
||||||
|
* PeriodFormat.seconds.list=\ sekunda%\ sekundy%\ sekund
|
||||||
|
* PeriodFormat.milliseconds.regex=^1$%[0-9]*(?<!1)[2-4]$%[0-9]*
|
||||||
|
* PeriodFormat.milliseconds.list=\ milisekunda%\ milisekundy%\ milisekund
|
||||||
|
* </pre>
|
||||||
|
* Each PeriodFormat.<duration_field_type>.regex property stands for an array of
|
||||||
|
* regular expressions and is followed by a property
|
||||||
|
* PeriodFormat.<duration_field_type>.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
257
src/main/java/org/xbib/time/format/PeriodFormatter.java
Normal file
257
src/main/java/org/xbib/time/format/PeriodFormatter.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1778
src/main/java/org/xbib/time/format/PeriodFormatterBuilder.java
Normal file
1778
src/main/java/org/xbib/time/format/PeriodFormatterBuilder.java
Normal file
File diff suppressed because it is too large
Load diff
32
src/main/java/org/xbib/time/format/PeriodParser.java
Normal file
32
src/main/java/org/xbib/time/format/PeriodParser.java
Normal 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);
|
||||||
|
|
||||||
|
}
|
55
src/main/java/org/xbib/time/format/PeriodPrinter.java
Normal file
55
src/main/java/org/xbib/time/format/PeriodPrinter.java
Normal 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 ≥ 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;
|
||||||
|
|
||||||
|
}
|
4
src/main/java/org/xbib/time/format/package-info.java
Normal file
4
src/main/java/org/xbib/time/format/package-info.java
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Classes for time formatting.
|
||||||
|
*/
|
||||||
|
package org.xbib.time.format;
|
18
src/main/java/org/xbib/time/pretty/LocaleAware.java
Normal file
18
src/main/java/org/xbib/time/pretty/LocaleAware.java
Normal 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);
|
||||||
|
|
||||||
|
}
|
468
src/main/java/org/xbib/time/pretty/PrettyTime.java
Normal file
468
src/main/java/org/xbib/time/pretty/PrettyTime.java
Normal 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
189
src/main/java/org/xbib/time/pretty/SimpleTimeFormat.java
Normal file
189
src/main/java/org/xbib/time/pretty/SimpleTimeFormat.java
Normal 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 + "]";
|
||||||
|
}
|
||||||
|
}
|
40
src/main/java/org/xbib/time/pretty/TimeFormat.java
Normal file
40
src/main/java/org/xbib/time/pretty/TimeFormat.java
Normal 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);
|
||||||
|
|
||||||
|
}
|
16
src/main/java/org/xbib/time/pretty/TimeFormatProvider.java
Normal file
16
src/main/java/org/xbib/time/pretty/TimeFormatProvider.java
Normal 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);
|
||||||
|
}
|
32
src/main/java/org/xbib/time/pretty/TimeUnit.java
Normal file
32
src/main/java/org/xbib/time/pretty/TimeUnit.java
Normal 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();
|
||||||
|
}
|
56
src/main/java/org/xbib/time/pretty/TimeUnitQuantity.java
Normal file
56
src/main/java/org/xbib/time/pretty/TimeUnitQuantity.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
102
src/main/java/org/xbib/time/pretty/i18n/MapResourceBundle.java
Normal file
102
src/main/java/org/xbib/time/pretty/i18n/MapResourceBundle.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
src/main/java/org/xbib/time/pretty/i18n/Resources.java
Normal file
111
src/main/java/org/xbib/time/pretty/i18n/Resources.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in a new issue