Initial version of Timex
timex is a datetime processing library allowing you to write time expressions in a DLS as strings. Splitting this out from Winchester as a separate library, since it could be useful elsewhere.
This commit is contained in:
commit
412eea1dbf
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
eggs
|
||||||
|
parts
|
||||||
|
var
|
||||||
|
sdist
|
||||||
|
develop-eggs
|
||||||
|
.installed.cfg
|
||||||
|
lib
|
||||||
|
lib64
|
||||||
|
__pycache__
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
.coverage
|
||||||
|
.tox
|
||||||
|
nosetests.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
202
LICENSE
Normal file
202
LICENSE
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.
|
||||||
|
|
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
include README.md
|
||||||
|
include requirements.txt
|
159
README.md
Normal file
159
README.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
timex
|
||||||
|
=====
|
||||||
|
|
||||||
|
A time expressions library implementing a mini-language for manipulating
|
||||||
|
datetimes.
|
||||||
|
|
||||||
|
Much like regular expressions provide a mini-language for performing certain
|
||||||
|
operation on strings, Timex's time expressions provide a convenient way of
|
||||||
|
expressing datetime and date-range operations. These expressions are strings,
|
||||||
|
and can be safely read from a config file or user input.
|
||||||
|
|
||||||
|
A simple example: Say you are writing code that creates a report on phone calls
|
||||||
|
to a store. You have a call log, perhaps in a database, with a timestamp for
|
||||||
|
each call, and you need to keep separate counts of calls that happened during
|
||||||
|
"employee hours", i.e. when employees should be there to answer the phone.
|
||||||
|
Current policy is that those are 30min before opening til an hour after closing.
|
||||||
|
That policy may change, however, so you don't want to hardcode it.
|
||||||
|
Here is code that will get you what you need:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
import timex
|
||||||
|
|
||||||
|
# store_open and store_close are datetimes for a specific day.
|
||||||
|
# calls is a list of object with a .timestamp attribute, that is also a
|
||||||
|
# datetime.
|
||||||
|
def count_calls(calls, store_open, store_close):
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
# This expression string could come from a config file.
|
||||||
|
time_expression = timex.parse("$opening - 30m to $closing + 1h")
|
||||||
|
|
||||||
|
# Call a compiled expression with keyword args to substitute needed
|
||||||
|
# variables.
|
||||||
|
matcher = time_expression(opening=store_open, closing=store_close)
|
||||||
|
|
||||||
|
for call in calls:
|
||||||
|
# Time matchers returned by calling an expression override the
|
||||||
|
# 'in' operator.
|
||||||
|
if call.timestamp in matcher:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Another example: Calculating expiration dates.
|
||||||
|
Say you need to calculate expiration dates for some items. There are several
|
||||||
|
types, and each has different rules:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
import timex
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# These rules could be in a database table or config file.
|
||||||
|
EXPIRATION_RULES = {
|
||||||
|
# This one expires at the beginning of the next day.
|
||||||
|
"thingy_type": "($timestamp + 1d) @ 0h 0m 0s",
|
||||||
|
# This one is only good for 3 hours
|
||||||
|
'whatzit_type': "$timestamp + 3h",
|
||||||
|
# Expires at noon on Dec, 31 of this year.
|
||||||
|
'foobar_type': "$timestamp @ 12mo 31d 12h 0m 0s"
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_expiration(items):
|
||||||
|
for item in items:
|
||||||
|
rule = EXPIRATION_RULES[item.type]
|
||||||
|
|
||||||
|
# In real code, you'd probably compile the rules outside the
|
||||||
|
# loop. It is fairly quick, though.
|
||||||
|
rule_expr = timex.parse(rule)
|
||||||
|
|
||||||
|
exp_timestamp = rule_expr(timestamp=datetime.now())
|
||||||
|
|
||||||
|
# You can access the datetime for a Timestamp matcher with
|
||||||
|
# .timestamp, .begin, or .end, as they will be the same.
|
||||||
|
# For TimeRanges, .begin is the start of the range, .end is the
|
||||||
|
# end of the range, and .timestamp is a synonym for .begin
|
||||||
|
item.expiration = exp_timestamp.timestamp
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Time Expressions
|
||||||
|
|
||||||
|
Time expressions can represent a single timestamp, calculated according to
|
||||||
|
the expression, or a range of times between a beginning datetime and an end.
|
||||||
|
In either case the usage is the same:
|
||||||
|
|
||||||
|
* Call timex.parse() to get an expression object from your string.
|
||||||
|
* Call the expression with any values you need as keyword args.
|
||||||
|
Note that there is a **default** variable, named _$timestamp_ that is
|
||||||
|
always available to your expressions. If you don't supply a value for it
|
||||||
|
as a keyword, then the value from _datetime.utcnow()_ is uesd.
|
||||||
|
* The above call gets you a Timestamp object or a TimeRange object,
|
||||||
|
depending on whether the expression represents a range, or a single
|
||||||
|
timestamp. Both of these have the same methods and attributes available.
|
||||||
|
You can compare a datetime to these objects using the .match() method,
|
||||||
|
or the _in_ operator (both will produce the same result), or access the
|
||||||
|
calculated datetimes with the .timestamp, .begin or .end attributes.
|
||||||
|
|
||||||
|
## Time Expression Reference and Syntax
|
||||||
|
|
||||||
|
### Duration:
|
||||||
|
|
||||||
|
A Duration is simply a number with a unit attached, like so:
|
||||||
|
`6h` or `30m`. Durations can also be ganged together like so `6h 30m 15s`
|
||||||
|
Valid units are:
|
||||||
|
|
||||||
|
| Unit | Description |
|
||||||
|
|:----:| ------------|
|
||||||
|
| y | Year |
|
||||||
|
| mo | Month |
|
||||||
|
| d | Day |
|
||||||
|
| h | Hour |
|
||||||
|
| m | Minute |
|
||||||
|
| s | Second |
|
||||||
|
| us | Microsecond |
|
||||||
|
|
||||||
|
### Timestamp expression:
|
||||||
|
|
||||||
|
Expression that evaluates to a single datetime.
|
||||||
|
|
||||||
|
### TimeRange expression:
|
||||||
|
|
||||||
|
Expression that evaluates to a range of time, represented by a begin
|
||||||
|
datetime and an end datetime. Addition, subtraction, or replace operations
|
||||||
|
on a range will perform the same operation on both the begin and end of the
|
||||||
|
timerange. Ranges can be created using the `to` operator, or with the special
|
||||||
|
range functions.
|
||||||
|
|
||||||
|
### Special (a.k.a "pinned") Time Ranges:
|
||||||
|
|
||||||
|
Special time ranges are generated with the special range functions `hour`, `day`, `month` and `year`.
|
||||||
|
For example the expression `day($start)` represents "the full day containing time $start". These ranges
|
||||||
|
are "special" because they remember the timestamp they are created from, and will "wrap around" on addition,
|
||||||
|
subtraction and replace operations so the timestamp is still within that range.
|
||||||
|
|
||||||
|
For example: If `$start` is "2014-08-01 01:00:00", then `day($start)` will be the
|
||||||
|
range "2014-08-01 00:00:00" to "2014-08-02 00:00:00", but `day($start) + 6h` will be the
|
||||||
|
range "2014-07-31 00:00:00" to "2014-08-01 06:00:00", so it will still contain $start.
|
||||||
|
|
||||||
|
This wrapping of the range will occure as long as the Duration added,
|
||||||
|
subtracted, or replaced is not larger than the range itself.
|
||||||
|
|
||||||
|
Note that the argument to one of these functions can be any timestamp expression, and if no argument is supplied,
|
||||||
|
it will default to the default variable `$timestamp`. Also, if no argument is supplied, the parens are not required,
|
||||||
|
like so `day + 6h`.
|
||||||
|
|
||||||
|
### Basic Syntax:
|
||||||
|
|
||||||
|
| Syntax | Meaning |
|
||||||
|
| ------------------------- | --------------------------------------------------------------------- |
|
||||||
|
| $variablename | Access value passed to expression as keyword arg. |
|
||||||
|
| () | Parentheses can be used to group expressions together for precedence. |
|
||||||
|
| `expression` + `duration` | Add time to a timestamp or range. |
|
||||||
|
| `expression` - `duration` | Subtract time from a timestamp or range. |
|
||||||
|
| `expression` @ `duration` | Replace operator for timestamp or range. Replaces component of datetime(s), similar to datetime's `replace` method |
|
||||||
|
| `timestamp1` to `timestamp2` | Create a time range beginning at `timestamp1` and ending at `timestamp2` |
|
||||||
|
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ply
|
||||||
|
six>=1.5.2
|
37
setup.py
Normal file
37
setup.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
from pip.req import parse_requirements
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def read(fname):
|
||||||
|
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||||
|
|
||||||
|
|
||||||
|
req_file = os.path.join(os.path.dirname(__file__), "requirements.txt")
|
||||||
|
install_reqs = [str(r.req) for r in parse_requirements(req_file)]
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='timex',
|
||||||
|
version='0.10.0',
|
||||||
|
author='Monsyne Dragon',
|
||||||
|
author_email='mdragon@rackspace.com',
|
||||||
|
description=("A time expressions library implementing a mini-language "
|
||||||
|
"for manipulating datetimes"),
|
||||||
|
license='Apache License (2.0)',
|
||||||
|
keywords='datetime manipulation transformation DSL',
|
||||||
|
packages=find_packages(exclude=['tests']),
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
'License :: OSI Approved :: Apache Software License',
|
||||||
|
'Operating System :: POSIX :: Linux',
|
||||||
|
'Programming Language :: Python :: 2.6',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
],
|
||||||
|
url='https://github.com/StackTach/timex',
|
||||||
|
scripts=[],
|
||||||
|
long_description=read('README.md'),
|
||||||
|
install_requires=install_reqs,
|
||||||
|
|
||||||
|
zip_safe=False
|
||||||
|
)
|
3
test-requirements.txt
Normal file
3
test-requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mock>=1.0
|
||||||
|
nose
|
||||||
|
unittest2
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
380
tests/test_expression.py
Normal file
380
tests/test_expression.py
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
#for Python2.6 compatability.
|
||||||
|
import unittest2 as unittest
|
||||||
|
import mock
|
||||||
|
import six
|
||||||
|
|
||||||
|
from timex import expression
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimestamp(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTimestamp, self).setUp()
|
||||||
|
self.dt = datetime.datetime(2014, 8, 1, 2 ,10, 23, 550)
|
||||||
|
self.other_dt = datetime.datetime(2014, 8, 7, 2 ,0, 0, 0)
|
||||||
|
self.timestamp = expression.Timestamp(self.dt)
|
||||||
|
|
||||||
|
def test_timestamp_properties(self):
|
||||||
|
self.assertEqual(self.dt, self.timestamp.begin)
|
||||||
|
self.assertEqual(self.dt, self.timestamp.end)
|
||||||
|
self.assertEqual(self.dt, self.timestamp.timestamp)
|
||||||
|
|
||||||
|
def test_match(self):
|
||||||
|
self.assertTrue(self.timestamp.match(self.dt))
|
||||||
|
self.assertFalse(self.timestamp.match(self.other_dt))
|
||||||
|
|
||||||
|
def test_in(self):
|
||||||
|
self.assertTrue(self.dt in self.timestamp)
|
||||||
|
self.assertFalse(self.other_dt in self.timestamp)
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 2, 10, 23, 560)
|
||||||
|
res = self.timestamp + expression.Duration(microsecond=10)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 2, 10, 33, 550)
|
||||||
|
res = self.timestamp + expression.Duration(second=10)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 2, 17, 23, 550)
|
||||||
|
res = self.timestamp + expression.Duration(minute=7)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 3, 10, 23, 550)
|
||||||
|
res = self.timestamp + expression.Duration(hour=1)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 3, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp + expression.Duration(day=2)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2015, 2, 1, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp + expression.Duration(month=6)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2017, 8, 1, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp + expression.Duration(year=3)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
def test_sub(self):
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 2, 10, 23, 540)
|
||||||
|
res = self.timestamp - expression.Duration(microsecond=10)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 2, 10, 13, 550)
|
||||||
|
res = self.timestamp - expression.Duration(second=10)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 2, 3, 23, 550)
|
||||||
|
res = self.timestamp - expression.Duration(minute=7)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 1, 10, 23, 550)
|
||||||
|
res = self.timestamp - expression.Duration(hour=1)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 7, 30, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp - expression.Duration(day=2)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 2, 1, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp - expression.Duration(month=6)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2011, 8, 1, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp - expression.Duration(year=3)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
def test_replace(self):
|
||||||
|
expected = datetime.datetime(2014, 8, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp % expression.Duration(day=2)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 6, 1, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp % expression.Duration(month=6)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2017, 8, 1, 2, 10, 23, 550)
|
||||||
|
res = self.timestamp % expression.Duration(year=2017)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
expected = datetime.datetime(2014, 8, 1, 0, 0, 0, 0)
|
||||||
|
res = self.timestamp % expression.Duration(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
self.assertEqual(res.timestamp, expected)
|
||||||
|
|
||||||
|
def test_handle_ambig_duration(self):
|
||||||
|
d = expression.Duration(hour=10, unknown=2)
|
||||||
|
self.assertRaises(expression.TimexExpressionError, self.timestamp.__add__, d)
|
||||||
|
self.assertRaises(expression.TimexExpressionError, self.timestamp.__sub__, d)
|
||||||
|
self.assertRaises(expression.TimexExpressionError, self.timestamp.__mod__, d)
|
||||||
|
|
||||||
|
def test_total_seconds(self):
|
||||||
|
self.assertFalse(self.timestamp.is_range)
|
||||||
|
self.assertEqual(self.timestamp.total_seconds(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeRange(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTimeRange, self).setUp()
|
||||||
|
self.begin_dt = datetime.datetime(2014, 8, 1, 2 ,10, 23, 550)
|
||||||
|
self.end_dt = datetime.datetime(2014, 8, 2, 2 ,10, 23, 550)
|
||||||
|
self.middle_dt = datetime.datetime(2014, 8, 1, 17 ,30, 10, 25)
|
||||||
|
self.other_dt = datetime.datetime(2014, 8, 7, 2 ,0, 0, 0)
|
||||||
|
self.timerange = expression.TimeRange(self.begin_dt, self.end_dt)
|
||||||
|
|
||||||
|
def test_timerange_properties(self):
|
||||||
|
self.assertEqual(self.begin_dt, self.timerange.begin)
|
||||||
|
self.assertEqual(self.end_dt, self.timerange.end)
|
||||||
|
self.assertEqual(self.begin_dt, self.timerange.timestamp)
|
||||||
|
|
||||||
|
def test_match(self):
|
||||||
|
#ranges include the beginning.
|
||||||
|
self.assertTrue(self.timerange.match(self.begin_dt))
|
||||||
|
self.assertTrue(self.timerange.match(self.middle_dt))
|
||||||
|
|
||||||
|
#ranges *don`t* include the end.
|
||||||
|
self.assertFalse(self.timerange.match(self.end_dt))
|
||||||
|
self.assertFalse(self.timerange.match(self.other_dt))
|
||||||
|
|
||||||
|
def test_in(self):
|
||||||
|
#ranges include the beginning.
|
||||||
|
self.assertTrue(self.begin_dt in self.timerange)
|
||||||
|
self.assertTrue(self.middle_dt in self.timerange)
|
||||||
|
|
||||||
|
#ranges *don`t* include the end.
|
||||||
|
self.assertFalse(self.end_dt in self.timerange)
|
||||||
|
self.assertFalse(self.other_dt in self.timerange)
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 10, 23, 560)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 10, 23, 560)
|
||||||
|
res = self.timerange + expression.Duration(microsecond=10)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 10, 33, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 10, 33, 550)
|
||||||
|
res = self.timerange + expression.Duration(second=10)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 17, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 17, 23, 550)
|
||||||
|
res = self.timerange + expression.Duration(minute=7)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 3, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 3, 10, 23, 550)
|
||||||
|
res = self.timerange + expression.Duration(hour=1)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 3, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 4, 2, 10, 23, 550)
|
||||||
|
res = self.timerange + expression.Duration(day=2)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2015, 2, 1, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2015, 2, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timerange + expression.Duration(month=6)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2017, 8, 1, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2017, 8, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timerange + expression.Duration(year=3)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
def test_sub(self):
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 10, 23, 540)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 10, 23, 540)
|
||||||
|
res = self.timerange - expression.Duration(microsecond=10)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 10, 13, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 10, 13, 550)
|
||||||
|
res = self.timerange - expression.Duration(second=10)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 3, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 3, 23, 550)
|
||||||
|
res = self.timerange - expression.Duration(minute=7)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 1, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 1, 10, 23, 550)
|
||||||
|
res = self.timerange - expression.Duration(hour=1)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 7, 30, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 7, 31, 2, 10, 23, 550)
|
||||||
|
res = self.timerange - expression.Duration(day=2)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 2, 1, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 2, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timerange - expression.Duration(month=6)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2011, 8, 1, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2011, 8, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timerange - expression.Duration(year=3)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace(self):
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 6, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 6, 10, 23, 550)
|
||||||
|
res = self.timerange % expression.Duration(hour=6)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 6, 1, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 6, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timerange % expression.Duration(month=6)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2017, 8, 1, 2, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2017, 8, 2, 2, 10, 23, 550)
|
||||||
|
res = self.timerange % expression.Duration(year=2017)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 0, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 0, 0, 0, 0)
|
||||||
|
res = self.timerange % expression.Duration(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
def test_handle_ambig_duration(self):
|
||||||
|
d = expression.Duration(unknown=1)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 3, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 3, 10, 23, 550)
|
||||||
|
res = self.timerange + d
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 1, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 1, 10, 23, 550)
|
||||||
|
res = self.timerange - d
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 1, 10, 23, 550)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 1, 10, 23, 550)
|
||||||
|
res = self.timerange % d
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
def test_total_seconds(self):
|
||||||
|
self.assertTrue(self.timerange.is_range)
|
||||||
|
self.assertEqual(self.timerange.total_seconds(), 24*60*60)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPinnedTimeRange(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPinnedTimeRange, self).setUp()
|
||||||
|
self.begin_dt = datetime.datetime(2014, 8, 1, 1, 0, 0, 0)
|
||||||
|
self.end_dt = datetime.datetime(2014, 8, 2, 1, 0, 0, 0)
|
||||||
|
self.middle_dt = datetime.datetime(2014, 8, 1, 17 ,30, 10, 25)
|
||||||
|
self.other_dt = datetime.datetime(2014, 8, 7, 2 ,0, 0, 0)
|
||||||
|
self.timerange = expression.PinnedTimeRange(self.begin_dt,
|
||||||
|
self.end_dt,
|
||||||
|
self.middle_dt,
|
||||||
|
'day')
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 0, 0, 0)
|
||||||
|
res = self.timerange + expression.Duration(hour=1)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 7, 31, 19, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 1, 19, 0, 0, 0)
|
||||||
|
res = self.timerange + expression.Duration(hour=18)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
def test_sub(self):
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 0, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 0, 0, 0, 0)
|
||||||
|
res = self.timerange - expression.Duration(hour=1)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 17, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 17, 0, 0, 0)
|
||||||
|
res = self.timerange - expression.Duration(hour=8)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
def test_replace(self):
|
||||||
|
expected_begin = datetime.datetime(2014, 8, 1, 2, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 2, 2, 0, 0, 0)
|
||||||
|
res = self.timerange % expression.Duration(hour=2)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
expected_begin = datetime.datetime(2014, 7, 31, 18, 0, 0, 0)
|
||||||
|
expected_end = datetime.datetime(2014, 8, 1, 18, 0, 0, 0)
|
||||||
|
res = self.timerange % expression.Duration(hour=18)
|
||||||
|
self.assertEqual(res.begin, expected_begin)
|
||||||
|
self.assertEqual(res.end, expected_end)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDuration(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDuration, self).setUp()
|
||||||
|
self.second = expression.Duration(second=1)
|
||||||
|
self.minute = expression.Duration(minute=1)
|
||||||
|
self.hour = expression.Duration(hour=1)
|
||||||
|
self.day = expression.Duration(day=1)
|
||||||
|
self.month = expression.Duration(month=1)
|
||||||
|
self.year = expression.Duration(year=1)
|
||||||
|
|
||||||
|
def test_gt(self):
|
||||||
|
self.assertTrue(self.hour > self.second)
|
||||||
|
self.assertFalse(self.second > self.hour)
|
||||||
|
|
||||||
|
def test_lt(self):
|
||||||
|
self.assertTrue(self.second < self.hour)
|
||||||
|
self.assertFalse(self.hour < self.second)
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
d = self.second + self.hour + self.day
|
||||||
|
self.assertEqual(d.second, 1)
|
||||||
|
self.assertEqual(d.hour, 1)
|
||||||
|
self.assertEqual(d.day, 1)
|
||||||
|
self.assertIsNone(d.microsecond)
|
||||||
|
self.assertIsNone(d.minute)
|
||||||
|
self.assertIsNone(d.month)
|
||||||
|
self.assertIsNone(d.year)
|
||||||
|
self.assertIsNone(d.unknown)
|
||||||
|
|
||||||
|
def test_as_dict(self):
|
||||||
|
d = expression.Duration(second=1, hour=1, day=1)
|
||||||
|
dd = d.as_dict
|
||||||
|
for unit in ('second', 'hour', 'day'):
|
||||||
|
self.assertIn(unit, dd)
|
||||||
|
self.assertEqual(dd[unit], 1)
|
||||||
|
for unit in ('microsecond', 'minute', 'month', 'year', 'unknown'):
|
||||||
|
self.assertNotIn(unit, dd)
|
||||||
|
|
132
tests/test_parse.py
Normal file
132
tests/test_parse.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
#for Python2.6 compatability.
|
||||||
|
import unittest2 as unittest
|
||||||
|
import mock
|
||||||
|
import six
|
||||||
|
|
||||||
|
import timex
|
||||||
|
|
||||||
|
|
||||||
|
class TestParse(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestParse, self).setUp()
|
||||||
|
self.dt = datetime.datetime(2014, 8, 1, 2 ,10, 23, 550)
|
||||||
|
self.other_dt = datetime.datetime(2014, 8, 7, 3, 20, 0, 0)
|
||||||
|
|
||||||
|
def test_var(self):
|
||||||
|
exp = timex.parse("$test_thingy")
|
||||||
|
t = exp(test_thingy=self.dt)
|
||||||
|
self.assertFalse(t.is_range)
|
||||||
|
self.assertEqual(t.timestamp, self.dt)
|
||||||
|
|
||||||
|
def test_timestamp_add(self):
|
||||||
|
result = datetime.datetime(2014, 8, 2, 4 ,10, 23, 550)
|
||||||
|
exp = timex.parse("$test_thingy + 1d 2h")
|
||||||
|
t = exp(test_thingy=self.dt)
|
||||||
|
self.assertFalse(t.is_range)
|
||||||
|
self.assertEqual(t.timestamp, result)
|
||||||
|
|
||||||
|
def test_timestamp_sub(self):
|
||||||
|
result = datetime.datetime(2014, 7, 31, 0 ,10, 23, 550)
|
||||||
|
exp = timex.parse("$test_thingy - 1d 2h")
|
||||||
|
t = exp(test_thingy=self.dt)
|
||||||
|
self.assertFalse(t.is_range)
|
||||||
|
self.assertEqual(t.timestamp, result)
|
||||||
|
|
||||||
|
def test_timestamp_replace(self):
|
||||||
|
result = datetime.datetime(2014, 8, 7, 6 ,10, 23, 550)
|
||||||
|
exp = timex.parse("$test_thingy @ 7d 6h")
|
||||||
|
t = exp(test_thingy=self.dt)
|
||||||
|
self.assertFalse(t.is_range)
|
||||||
|
self.assertEqual(t.timestamp, result)
|
||||||
|
|
||||||
|
def test_timerange(self):
|
||||||
|
exp = timex.parse("$test_thingy to $other")
|
||||||
|
t = exp(test_thingy=self.dt, other=self.other_dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, self.dt)
|
||||||
|
self.assertEqual(t.end, self.other_dt)
|
||||||
|
|
||||||
|
def test_timerange_add(self):
|
||||||
|
result_begin = datetime.datetime(2014, 8, 2, 4 ,10, 23, 550)
|
||||||
|
result_end = datetime.datetime(2014, 8, 8, 5, 20, 0, 0)
|
||||||
|
exp = timex.parse("($test_thingy to $other) + 1d 2h")
|
||||||
|
t = exp(test_thingy=self.dt, other=self.other_dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_timerange_sub(self):
|
||||||
|
result_begin = datetime.datetime(2014, 7, 31, 0 ,10, 23, 550)
|
||||||
|
result_end = datetime.datetime(2014, 8, 6, 1, 20, 0, 0)
|
||||||
|
exp = timex.parse("($test_thingy to $other) - 1d 2h")
|
||||||
|
t = exp(test_thingy=self.dt, other=self.other_dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_timerange_replace(self):
|
||||||
|
result_begin = datetime.datetime(2014, 8, 1, 6, 10, 23, 550)
|
||||||
|
result_end = datetime.datetime(2014, 8, 7, 6, 20, 0, 0)
|
||||||
|
exp = timex.parse("($test_thingy to $other) @ 6h")
|
||||||
|
t = exp(test_thingy=self.dt, other=self.other_dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_special_range(self):
|
||||||
|
result_begin = datetime.datetime(2014, 8, 1, 0, 0, 0, 0)
|
||||||
|
result_end = datetime.datetime(2014, 8, 2, 0, 0, 0, 0)
|
||||||
|
exp = timex.parse("day($test_thingy)")
|
||||||
|
t = exp(test_thingy=self.dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
exp = timex.parse("day")
|
||||||
|
t = exp(timestamp=self.dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_special_range_wrap_replace(self):
|
||||||
|
result_begin = datetime.datetime(2014, 7, 31, 6, 0, 0, 0)
|
||||||
|
result_end = datetime.datetime(2014, 8, 1, 6, 0, 0, 0)
|
||||||
|
exp = timex.parse("day @ 6h")
|
||||||
|
t = exp(timestamp=self.dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_special_range_wrap_add(self):
|
||||||
|
result_begin = datetime.datetime(2014, 7, 31, 6, 0, 0, 0)
|
||||||
|
result_end = datetime.datetime(2014, 8, 1, 6, 0, 0, 0)
|
||||||
|
exp = timex.parse("day + 6h")
|
||||||
|
t = exp(timestamp=self.dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_special_range_wrap_sub(self):
|
||||||
|
result_begin = datetime.datetime(2014, 8, 1, 18, 0, 0, 0)
|
||||||
|
result_end = datetime.datetime(2014, 8, 2, 18, 0, 0, 0)
|
||||||
|
exp = timex.parse("day - 6h")
|
||||||
|
t = exp(timestamp=datetime.datetime(2014, 8, 1, 19, 45, 30, 225))
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
||||||
|
def test_timerange_ambig_duration(self):
|
||||||
|
# Ambiguous durations are a bit of a hack to make timex syntax
|
||||||
|
# compatable with the (much less flexible) syntax for timeranges
|
||||||
|
# used for some OpenStack projects. (mdragon)
|
||||||
|
result_begin = datetime.datetime(2014, 8, 1, 2, 0, 0, 0)
|
||||||
|
result_end = datetime.datetime(2014, 8, 2, 2, 0, 0, 0)
|
||||||
|
exp = timex.parse("day @ 2")
|
||||||
|
t = exp(timestamp=self.dt)
|
||||||
|
self.assertTrue(t.is_range)
|
||||||
|
self.assertEqual(t.begin, result_begin)
|
||||||
|
self.assertEqual(t.end, result_end)
|
||||||
|
|
7
timex/__init__.py
Normal file
7
timex/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from timex.parser import parse
|
||||||
|
from timex.expression import TimexExpressionError, TimexParserError
|
||||||
|
from timex.expression import TimexLexerError, TimexError
|
||||||
|
from timex.expression import Timestamp, TimeRange, PinnedTimeRange, Duration
|
||||||
|
|
||||||
|
__version__ = '0.10.0'
|
||||||
|
|
479
timex/expression.py
Normal file
479
timex/expression.py
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
import logging
|
||||||
|
import datetime
|
||||||
|
import abc
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TimexError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimexLexerError(TimexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimexParserError(TimexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimexExpressionError(TimexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class TimeMatcher(object):
|
||||||
|
|
||||||
|
_allow_ambig_duration = False
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def match(self, dt):
|
||||||
|
"""Does a specific datetime match this?"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __add__(self, other):
|
||||||
|
"""Add a duration"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __sub__(self, other):
|
||||||
|
"""Subtract a duration"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __mod__(self, other):
|
||||||
|
"""Implements the replace operation with a duration"""
|
||||||
|
|
||||||
|
def total_seconds(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_range(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __contains__(self, other):
|
||||||
|
return self.match(other)
|
||||||
|
|
||||||
|
def _check_duration(self, duration):
|
||||||
|
if isinstance(duration, Duration):
|
||||||
|
if ((duration.ambiguous and self._allow_ambig_duration)
|
||||||
|
or not duration.ambiguous):
|
||||||
|
return True
|
||||||
|
raise TimexExpressionError("Invalid duration for time operation")
|
||||||
|
|
||||||
|
|
||||||
|
def _dt_replace(self, dt, duration):
|
||||||
|
return dt.replace(**duration.as_dict)
|
||||||
|
|
||||||
|
def _dt_add(self, dt, duration):
|
||||||
|
d = duration.as_dict
|
||||||
|
months = d.pop('month', 0)
|
||||||
|
years = d.pop('year', 0)
|
||||||
|
if d:
|
||||||
|
delta = datetime.timedelta(**dict((k+"s",val) for k, val in d.items()))
|
||||||
|
dt = dt + delta
|
||||||
|
if months:
|
||||||
|
newmonth = dt.month + months
|
||||||
|
years += (newmonth - 1) // 12
|
||||||
|
newmonth = ((newmonth-1) % 12) + 1
|
||||||
|
dt = dt.replace(month=newmonth)
|
||||||
|
if years:
|
||||||
|
dt = dt.replace(year=(dt.year+years))
|
||||||
|
return dt
|
||||||
|
|
||||||
|
def _dt_sub(self, dt, duration):
|
||||||
|
d = duration.as_dict
|
||||||
|
months = d.pop('month', 0)
|
||||||
|
years = d.pop('year', 0)
|
||||||
|
if d:
|
||||||
|
delta = datetime.timedelta(**dict((k+"s",val) for k, val in d.items()))
|
||||||
|
dt = dt - delta
|
||||||
|
if months:
|
||||||
|
newmonth = dt.month - months
|
||||||
|
years -= (newmonth - 1) // 12
|
||||||
|
newmonth = ((newmonth-1) % 12) + 1
|
||||||
|
dt = dt.replace(month=newmonth)
|
||||||
|
if years:
|
||||||
|
dt = dt.replace(year=(dt.year-years))
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
class Timestamp(TimeMatcher):
|
||||||
|
"""This is a wrapper on a datetime that has the same
|
||||||
|
interface as TimeRange"""
|
||||||
|
|
||||||
|
def __init__(self, dt):
|
||||||
|
self.timestamp = dt
|
||||||
|
|
||||||
|
@property
|
||||||
|
def begin(self):
|
||||||
|
return self.timestamp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def end(self):
|
||||||
|
return self.timestamp
|
||||||
|
|
||||||
|
def match(self, dt):
|
||||||
|
return self.timestamp == dt
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
self._check_duration(other)
|
||||||
|
return Timestamp(self._dt_add(self.timestamp, other))
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
self._check_duration(other)
|
||||||
|
return Timestamp(self._dt_sub(self.timestamp, other))
|
||||||
|
|
||||||
|
def __mod__(self, other):
|
||||||
|
self._check_duration(other)
|
||||||
|
return Timestamp(self._dt_replace(self.timestamp, other))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Timestamp for %r" % self.timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRange(TimeMatcher):
|
||||||
|
|
||||||
|
_allow_ambig_duration = True
|
||||||
|
|
||||||
|
def __init__(self, begin, end):
|
||||||
|
self.begin = begin
|
||||||
|
self.end = end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self):
|
||||||
|
return self.begin
|
||||||
|
|
||||||
|
def total_seconds(self):
|
||||||
|
delta = self.end - self.begin
|
||||||
|
return delta.seconds + (delta.days * 24 * 3600) + (delta.microseconds * 1e-6)
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return self.total_seconds() > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_range(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def match(self, dt):
|
||||||
|
"""TimeRanges match datetimes from begin (inclusive) to end (exclusive)"""
|
||||||
|
return dt >= self.begin and dt < self.end
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
self._check_duration(other)
|
||||||
|
duration = other.in_context(self)
|
||||||
|
begin = self._dt_add(self.begin, duration)
|
||||||
|
end = self._dt_add(self.end, duration)
|
||||||
|
return TimeRange(begin, end)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
self._check_duration(other)
|
||||||
|
duration = other.in_context(self)
|
||||||
|
begin = self._dt_sub(self.begin, duration)
|
||||||
|
end = self._dt_sub(self.end, duration)
|
||||||
|
return TimeRange(begin, end)
|
||||||
|
|
||||||
|
def __mod__(self, other):
|
||||||
|
self._check_duration(other)
|
||||||
|
duration = other.in_context(self)
|
||||||
|
begin = self._dt_replace(self.begin, duration)
|
||||||
|
end = self._dt_replace(self.end, duration)
|
||||||
|
return TimeRange(begin, end)
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
begin = self.end
|
||||||
|
end = self._dt_add(begin, Duration(second=self.total_seconds()))
|
||||||
|
return TimeRange(begin, end)
|
||||||
|
|
||||||
|
def prev(self):
|
||||||
|
end = self.begin
|
||||||
|
begin = self._dt_sub(end, Duration(second=self.total_seconds()))
|
||||||
|
return TimeRange(begin, end)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "TimeRange from %r to %r" % (self.begin, self.end)
|
||||||
|
|
||||||
|
def pin(self, dt, unit):
|
||||||
|
return PinnedTimeRange(self.begin, self.end, dt, unit)
|
||||||
|
|
||||||
|
|
||||||
|
class PinnedTimeRange(TimeRange):
|
||||||
|
|
||||||
|
def __init__(self, begin, end, pinned_to, unit):
|
||||||
|
super(PinnedTimeRange, self).__init__(begin, end)
|
||||||
|
self.pinned_to = pinned_to
|
||||||
|
self.unit = unit
|
||||||
|
self.duration = Duration(**{unit: 1})
|
||||||
|
|
||||||
|
def _pin_adjust(self, time_range):
|
||||||
|
if self.pinned_to in time_range:
|
||||||
|
return time_range.pin(self.pinned_to, self.unit)
|
||||||
|
while time_range.begin > self.pinned_to:
|
||||||
|
time_range = time_range.prev()
|
||||||
|
while time_range.end <= self.pinned_to:
|
||||||
|
time_range = time_range.next()
|
||||||
|
return time_range.pin(self.pinned_to, self.unit)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
time_range = super(PinnedTimeRange, self).__add__(other)
|
||||||
|
if other < self.duration:
|
||||||
|
return self._pin_adjust(time_range)
|
||||||
|
return self.time_range
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
time_range = super(PinnedTimeRange, self).__sub__(other)
|
||||||
|
if other < self.duration:
|
||||||
|
return self._pin_adjust(time_range)
|
||||||
|
return self.time_range
|
||||||
|
|
||||||
|
def __mod__(self, other):
|
||||||
|
time_range = super(PinnedTimeRange, self).__mod__(other)
|
||||||
|
if other < self.duration:
|
||||||
|
return self._pin_adjust(time_range)
|
||||||
|
return self.time_range
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "PinnedTimeRange from %r to %r. Pinned to %s(%r)" % (self.begin, self.end, self.unit, self.pinned_to)
|
||||||
|
|
||||||
|
|
||||||
|
class Environment(dict):
|
||||||
|
|
||||||
|
def time_func_hour(self, timestamp):
|
||||||
|
dt = timestamp.timestamp
|
||||||
|
begin = dt.replace(minute=0, second=0, microsecond=0)
|
||||||
|
end = begin + datetime.timedelta(hours=1)
|
||||||
|
return PinnedTimeRange(begin, end, dt, 'hour')
|
||||||
|
|
||||||
|
def time_func_day(self, timestamp):
|
||||||
|
dt = timestamp.timestamp
|
||||||
|
begin = dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
end = begin + datetime.timedelta(days=1)
|
||||||
|
return PinnedTimeRange(begin, end, dt, 'day')
|
||||||
|
|
||||||
|
def time_func_month(self, timestamp):
|
||||||
|
dt = timestamp.timestamp
|
||||||
|
begin = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
end = Timestamp(begin) + Duration(month=1)
|
||||||
|
return PinnedTimeRange(begin, end.timestamp, dt, 'month')
|
||||||
|
|
||||||
|
def time_func_year(self, timestamp):
|
||||||
|
dt = timestamp.timestamp
|
||||||
|
begin = dt.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
end = Timestamp(begin) + Duration(year=1)
|
||||||
|
return PinnedTimeRange(begin, end.timestamp, dt, 'year')
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class TimeExpression(object):
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def apply(self, env):
|
||||||
|
"""Apply the expression to a given set of arguments.
|
||||||
|
|
||||||
|
:param env: a dictionary-like object. expression functions should be methods
|
||||||
|
on this object with names beginning with 'time_func_'
|
||||||
|
returns: TimeMatcher instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, **kw):
|
||||||
|
env = Environment()
|
||||||
|
env.update(kw)
|
||||||
|
if 'timestamp' not in env:
|
||||||
|
env['timestamp'] = datetime.datetime.utcnow()
|
||||||
|
return self.apply(env)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRangeExpression(TimeExpression):
|
||||||
|
def __init__(self, begin, end):
|
||||||
|
self.begin = begin
|
||||||
|
self.end = end
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s(%r, %r)' % (self.__class__.__name__, self.begin, self.end)
|
||||||
|
|
||||||
|
def apply(self, env):
|
||||||
|
begin = self.begin.apply(env)
|
||||||
|
end = self.end.apply(env)
|
||||||
|
return TimeRange(begin.timestamp, end.timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRangeFunction(TimeRangeExpression):
|
||||||
|
def __init__(self, func_name, expr=None):
|
||||||
|
self.func_name = func_name
|
||||||
|
if expr is None:
|
||||||
|
expr = Variable('timestamp')
|
||||||
|
self.expr = expr
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s %s(%r)' % (self.__class__.__name__, self.func_name, self.expr)
|
||||||
|
|
||||||
|
def apply(self, env):
|
||||||
|
arg = self.expr.apply(env)
|
||||||
|
try:
|
||||||
|
func = getattr(env, "time_func_%s" % self.func_name)
|
||||||
|
except AttributeError:
|
||||||
|
raise TimexExpressionError("Unknown Function %s" % self.func_name)
|
||||||
|
return func(arg)
|
||||||
|
|
||||||
|
|
||||||
|
class Variable(TimeExpression):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s (%s)" % (self.__class__.__name__, self.name)
|
||||||
|
|
||||||
|
def apply(self, env):
|
||||||
|
if self.name not in env:
|
||||||
|
raise TimexExpressionError("Variable %s not defined" % self.name)
|
||||||
|
return Timestamp(env[self.name])
|
||||||
|
|
||||||
|
|
||||||
|
class Operation(TimeExpression):
|
||||||
|
def __init__(self, expr, duration):
|
||||||
|
if duration.ambiguous and not isinstance(expr, TimeRangeExpression):
|
||||||
|
raise TimexParserError("Durations must have unit for "
|
||||||
|
"TimestampExpressions")
|
||||||
|
self.expr = expr
|
||||||
|
self.duration = duration
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s(%r, %r)' % (self.__class__.__name__, self.expr, self.duration)
|
||||||
|
|
||||||
|
|
||||||
|
class Replace(Operation):
|
||||||
|
|
||||||
|
def apply(self, env):
|
||||||
|
val = self.expr.apply(env)
|
||||||
|
return val % self.duration
|
||||||
|
|
||||||
|
|
||||||
|
class Plus(Operation):
|
||||||
|
|
||||||
|
def apply(self, env):
|
||||||
|
val = self.expr.apply(env)
|
||||||
|
return val + self.duration
|
||||||
|
|
||||||
|
|
||||||
|
class Minus(Operation):
|
||||||
|
|
||||||
|
def apply(self, env):
|
||||||
|
val = self.expr.apply(env)
|
||||||
|
return val - self.duration
|
||||||
|
|
||||||
|
|
||||||
|
class Duration(object):
|
||||||
|
|
||||||
|
UNIT_SIZES = {'year': 365*24*60*60,
|
||||||
|
'month': 28*24*60*60,
|
||||||
|
'day': 24*60*60,
|
||||||
|
'hour': 60*60,
|
||||||
|
'minute': 60,
|
||||||
|
'second': 1,
|
||||||
|
'microsecond': 1e-6}
|
||||||
|
UNITS = ('year',
|
||||||
|
'month',
|
||||||
|
'day',
|
||||||
|
'hour',
|
||||||
|
'minute',
|
||||||
|
'second',
|
||||||
|
'microsecond',)
|
||||||
|
|
||||||
|
def __init__(self, year=None, month=None, day=None, hour=None,
|
||||||
|
minute=None, second=None, microsecond=None, unknown=None):
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
self.day = day
|
||||||
|
self.hour = hour
|
||||||
|
self.minute = minute
|
||||||
|
self.second = second
|
||||||
|
self.microsecond = microsecond
|
||||||
|
self.unknown = unknown
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ambiguous(self):
|
||||||
|
return self.unknown is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_dict(self):
|
||||||
|
d = dict()
|
||||||
|
for unit in self.UNITS:
|
||||||
|
val = getattr(self, unit)
|
||||||
|
if val is not None:
|
||||||
|
d[unit] = val
|
||||||
|
if self.ambiguous:
|
||||||
|
d['unknown'] = self.unknown
|
||||||
|
return d
|
||||||
|
|
||||||
|
def in_context(self, timerange):
|
||||||
|
if not self.ambiguous:
|
||||||
|
return self
|
||||||
|
d = abs(timerange.total_seconds())
|
||||||
|
if d >= self.UNIT_SIZES['year']:
|
||||||
|
unit = 'month'
|
||||||
|
elif d >= self.UNIT_SIZES['month']:
|
||||||
|
unit = 'day'
|
||||||
|
elif d >= self.UNIT_SIZES['day']:
|
||||||
|
unit = 'hour'
|
||||||
|
elif d >= self.UNIT_SIZES['hour']:
|
||||||
|
unit = 'minute'
|
||||||
|
elif d >= self.UNIT_SIZES['minute']:
|
||||||
|
unit = 'second'
|
||||||
|
else:
|
||||||
|
unit = microsecond
|
||||||
|
vals = self.as_dict
|
||||||
|
del vals['unknown']
|
||||||
|
if unit in vals:
|
||||||
|
vals[unit] += self.unknown
|
||||||
|
else:
|
||||||
|
vals[unit] = self.unknown
|
||||||
|
return Duration(**vals)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
result = self.as_dict
|
||||||
|
o = other.as_dict
|
||||||
|
for unit in o:
|
||||||
|
if unit in result:
|
||||||
|
result[unit] += o[unit]
|
||||||
|
else:
|
||||||
|
result[unit] = o[unit]
|
||||||
|
return Duration(**result)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s %s' % (self.__class__.__name__, str(self.as_dict))
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
for unit in self.UNITS:
|
||||||
|
our_val = getattr(self, unit)
|
||||||
|
other_val = getattr(other, unit)
|
||||||
|
if our_val is not None and other_val is not None:
|
||||||
|
return our_val > other_val
|
||||||
|
elif our_val is not None and other_val is None:
|
||||||
|
return True
|
||||||
|
elif our_val is None and other_val is not None:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
for unit in self.UNITS:
|
||||||
|
our_val = getattr(self, unit)
|
||||||
|
other_val = getattr(other, unit)
|
||||||
|
if our_val is not None and other_val is not None:
|
||||||
|
return our_val < other_val
|
||||||
|
elif our_val is not None and other_val is None:
|
||||||
|
return False
|
||||||
|
elif our_val is None and other_val is not None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
for unit in self.UNITS:
|
||||||
|
our_val = getattr(self, unit)
|
||||||
|
other_val = getattr(other, unit)
|
||||||
|
if our_val != other_val:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
102
timex/lexer.py
Normal file
102
timex/lexer.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import ply.lex
|
||||||
|
|
||||||
|
from timex.expression import TimexLexerError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TimexLexer(object):
|
||||||
|
"""Lexing/tokenising for time expressions"""
|
||||||
|
|
||||||
|
def __init__(self, debug=False):
|
||||||
|
self.debug = debug
|
||||||
|
if not self.__doc__:
|
||||||
|
raise TimexLexerError("Docstring information is missing. "
|
||||||
|
"Timex uses PLY which requires docstrings for "
|
||||||
|
"configuration.")
|
||||||
|
self.lexer = ply.lex.lex(module=self,
|
||||||
|
debug=self.debug,
|
||||||
|
errorlog=logger)
|
||||||
|
self.lexer.string_value = None
|
||||||
|
self.latest_newline = 0
|
||||||
|
|
||||||
|
def input(self, string):
|
||||||
|
self.lexer.input(string)
|
||||||
|
|
||||||
|
def token(self):
|
||||||
|
token = self.lexer.token()
|
||||||
|
if token is None:
|
||||||
|
if self.lexer.string_value is not None:
|
||||||
|
raise TimexLexerError("Unexpected EOF in expression")
|
||||||
|
else:
|
||||||
|
token.col = token.lexpos - self.latest_newline
|
||||||
|
return token
|
||||||
|
|
||||||
|
reserved_words = { 'to' : 'TO',
|
||||||
|
'us' : 'MICROSECOND',
|
||||||
|
's' : 'SECOND',
|
||||||
|
'sec' : 'SECOND',
|
||||||
|
'm' : 'MINUTE',
|
||||||
|
'min' : 'MINUTE',
|
||||||
|
'h' : 'HOUR',
|
||||||
|
'hr' : 'HOUR',
|
||||||
|
'd' : 'DAY',
|
||||||
|
'mo' : 'MONTH',
|
||||||
|
'y' : 'YEAR',
|
||||||
|
'yr' : 'YEAR',
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens = ('NUMBER',
|
||||||
|
'PLUS',
|
||||||
|
'MINUS',
|
||||||
|
'REPLACE',
|
||||||
|
'RPAREN',
|
||||||
|
'LPAREN',
|
||||||
|
'VAR',
|
||||||
|
'IDENTIFIER') + tuple(set(reserved_words.values()))
|
||||||
|
|
||||||
|
t_PLUS = r'\+'
|
||||||
|
t_MINUS = r'-'
|
||||||
|
t_REPLACE = r'@'
|
||||||
|
t_VAR = r'\$'
|
||||||
|
t_LPAREN = r'\('
|
||||||
|
t_RPAREN = r'\)'
|
||||||
|
|
||||||
|
def t_IDENTIFIER(self, t):
|
||||||
|
r'[a-zA-Z_][a-zA-Z0-9_]*'
|
||||||
|
t.type = self.reserved_words.get(t.value, 'IDENTIFIER')
|
||||||
|
return t
|
||||||
|
|
||||||
|
def t_NUMBER(self, t):
|
||||||
|
r'\d+'
|
||||||
|
t.value = int(t.value)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def t_newline(self, t):
|
||||||
|
r'\n+'
|
||||||
|
t.lexer.lineno += len(t.value)
|
||||||
|
self.latest_newline = t.lexpos
|
||||||
|
|
||||||
|
t_ignore = ' \t'
|
||||||
|
|
||||||
|
def t_error(self, t):
|
||||||
|
raise TimexLexerError('Error on line %s, col %s: Unexpected character:'
|
||||||
|
' %s ' % (t.lexer.lineno,
|
||||||
|
t.lexpos - self.latest_newline,
|
||||||
|
t.value[0]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig()
|
||||||
|
lexer = TimexLexer(debug=True)
|
||||||
|
lexer.input(sys.stdin.read())
|
||||||
|
token = lexer.token()
|
||||||
|
while token:
|
||||||
|
print('%-20s%s' % (token.value, token.type))
|
||||||
|
token = lexer.token()
|
||||||
|
|
195
timex/parser.py
Normal file
195
timex/parser.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import sys, os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import ply.yacc
|
||||||
|
|
||||||
|
from timex.lexer import TimexLexer
|
||||||
|
from timex.expression import TimexParserError
|
||||||
|
from timex.expression import Replace, Plus, Minus
|
||||||
|
from timex.expression import Duration, Variable
|
||||||
|
from timex.expression import TimeRangeFunction, TimeRangeExpression
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parse Time Expression:
|
||||||
|
|
||||||
|
BNF:
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
time_expression : timerange_expression
|
||||||
|
| timestamp_expression
|
||||||
|
|
||||||
|
timerange_expression : timestamp_expression TO timestamp_expression
|
||||||
|
| timestamp_expression PLUS duration
|
||||||
|
| timestamp_expression MINUS duration
|
||||||
|
| timerange_expression REPLACE duration
|
||||||
|
| LPAREN timerange_expression RPAREN
|
||||||
|
| range_function
|
||||||
|
|
||||||
|
timestamp_expression : timestamp_expression PLUS duration
|
||||||
|
| timestamp_expression MINUS duration
|
||||||
|
| timestamp_expression REPLACE duration
|
||||||
|
| LPAREN timestamp_expression RPAREN
|
||||||
|
| variable
|
||||||
|
|
||||||
|
range_function : IDENTIFIER LPAREN timestamp_expression RPAREN
|
||||||
|
| IDENTIFIER
|
||||||
|
|
||||||
|
variable : VAR IDENTIFIER
|
||||||
|
|
||||||
|
duration : duration duration
|
||||||
|
| NUMBER unit
|
||||||
|
| NUMBER
|
||||||
|
|
||||||
|
unit : SECOND
|
||||||
|
| MICROSECOND
|
||||||
|
| MINUTE
|
||||||
|
| HOUR
|
||||||
|
| DAY
|
||||||
|
| MONTH
|
||||||
|
| YEAR
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(string):
|
||||||
|
return TimexParser().parse(string)
|
||||||
|
|
||||||
|
|
||||||
|
class TimexParser(object):
|
||||||
|
""" LALR parser for time expression mini-language."""
|
||||||
|
tokens = TimexLexer.tokens
|
||||||
|
|
||||||
|
def __init__(self, debug=False, lexer_class=None, start='time_expression'):
|
||||||
|
self.debug = debug
|
||||||
|
self.start = start
|
||||||
|
if not self.__doc__:
|
||||||
|
raise TimexParserError("Docstring information is missing. "
|
||||||
|
"Timex uses PLY which requires docstrings for "
|
||||||
|
"configuration.")
|
||||||
|
self.lexer_class = lexer_class or TimexLexer
|
||||||
|
|
||||||
|
def _parse_table(self):
|
||||||
|
tabdir = os.path.dirname(__file__)
|
||||||
|
try:
|
||||||
|
module_name = os.path.splitext(os.path.split(__file__)[1])[0]
|
||||||
|
except:
|
||||||
|
module_name = __name__
|
||||||
|
table_module = '_'.join([module_name, self.start, 'parsetab'])
|
||||||
|
return (tabdir, table_module)
|
||||||
|
|
||||||
|
def parse(self, string):
|
||||||
|
lexer = self.lexer_class(debug=self.debug)
|
||||||
|
|
||||||
|
tabdir, table_module = self._parse_table()
|
||||||
|
parser = ply.yacc.yacc(module=self,
|
||||||
|
debug=self.debug,
|
||||||
|
tabmodule = table_module,
|
||||||
|
outputdir = tabdir,
|
||||||
|
write_tables=0,
|
||||||
|
start = self.start,
|
||||||
|
errorlog = logger)
|
||||||
|
|
||||||
|
return parser.parse(string, lexer=lexer)
|
||||||
|
|
||||||
|
precedence = [
|
||||||
|
('left', 'TO'),
|
||||||
|
('left', 'PLUS', 'MINUS'),
|
||||||
|
('left', 'REPLACE'),
|
||||||
|
('right', 'VAR'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def p_error(self, t):
|
||||||
|
raise TimexParserError('Parse error at %s:%s near token %s (%s)' %
|
||||||
|
(t.lineno, t.col, t.value, t.type))
|
||||||
|
|
||||||
|
def p_time_expression(self, p):
|
||||||
|
"""time_expression : timerange_expression
|
||||||
|
| timestamp_expression
|
||||||
|
"""
|
||||||
|
p[0] = p[1]
|
||||||
|
|
||||||
|
def p_timerange_to(self, p):
|
||||||
|
'timerange_expression : timestamp_expression TO timestamp_expression'
|
||||||
|
p[0] = TimeRangeExpression(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timerange_replace(self, p):
|
||||||
|
"""timerange_expression : timerange_expression REPLACE duration"""
|
||||||
|
p[0] = Replace(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timerange_plus(self, p):
|
||||||
|
"""timerange_expression : timerange_expression PLUS duration"""
|
||||||
|
p[0] = Plus(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timerange_minus(self, p):
|
||||||
|
"""timerange_expression : timerange_expression MINUS duration"""
|
||||||
|
p[0] = Minus(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timerange_parens(self, p):
|
||||||
|
"""timerange_expression : LPAREN timerange_expression RPAREN"""
|
||||||
|
p[0] = p[2]
|
||||||
|
|
||||||
|
def p_timerange_function(self, p):
|
||||||
|
"""timerange_expression : range_function"""
|
||||||
|
p[0] = p[1]
|
||||||
|
|
||||||
|
def p_timestamp_replace(self, p):
|
||||||
|
"""timestamp_expression : timestamp_expression REPLACE duration"""
|
||||||
|
p[0] = Replace(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timestamp_plus(self, p):
|
||||||
|
"""timestamp_expression : timestamp_expression PLUS duration"""
|
||||||
|
p[0] = Plus(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timestamp_minus(self, p):
|
||||||
|
"""timestamp_expression : timestamp_expression MINUS duration"""
|
||||||
|
p[0] = Minus(p[1], p[3])
|
||||||
|
|
||||||
|
def p_timestamp_parens(self, p):
|
||||||
|
"""timestamp_expression : LPAREN timestamp_expression RPAREN"""
|
||||||
|
p[0] = p[2]
|
||||||
|
|
||||||
|
def p_timestamp_variable(self, p):
|
||||||
|
"""timestamp_expression : variable"""
|
||||||
|
p[0] = p[1]
|
||||||
|
|
||||||
|
def p_range_function_expr(self, p):
|
||||||
|
"""range_function : IDENTIFIER LPAREN timestamp_expression RPAREN"""
|
||||||
|
p[0] = TimeRangeFunction(p[1], p[3])
|
||||||
|
|
||||||
|
def p_range_function(self, p):
|
||||||
|
"""range_function : IDENTIFIER"""
|
||||||
|
p[0] = TimeRangeFunction(p[1])
|
||||||
|
|
||||||
|
def p_varible(self, p):
|
||||||
|
'variable : VAR IDENTIFIER'
|
||||||
|
p[0] = Variable(p[2])
|
||||||
|
|
||||||
|
def p_duration_unit(self, p):
|
||||||
|
"""duration : NUMBER unit"""
|
||||||
|
p[0] = Duration(**{p[2]: p[1]})
|
||||||
|
|
||||||
|
def p_duration_number(self, p):
|
||||||
|
"""duration : NUMBER"""
|
||||||
|
p[0] = Duration(unknown=p[1])
|
||||||
|
|
||||||
|
def p_duration_duration(self, p):
|
||||||
|
"""duration : duration duration"""
|
||||||
|
p[0] = p[1] + p[2]
|
||||||
|
|
||||||
|
def p_unit(self, p):
|
||||||
|
"""unit : SECOND
|
||||||
|
| MICROSECOND
|
||||||
|
| MINUTE
|
||||||
|
| HOUR
|
||||||
|
| DAY
|
||||||
|
| MONTH
|
||||||
|
| YEAR"""
|
||||||
|
unit = TimexLexer.reserved_words[p[1]]
|
||||||
|
unit = unit.lower()
|
||||||
|
p[0] = unit
|
Loading…
Reference in New Issue
Block a user