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