├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── requirements.txt └── tests.py └── timestring ├── Date.py ├── Range.py ├── __init__.py ├── text2num.py └── timestring_re.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = timestring 3 | branch = True 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | def __repr__ 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | ignore_errors = True 12 | omit = 13 | venv/* 14 | tests/* 15 | _* 16 | setup.py 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | lib 17 | lib64 18 | __pycache__ 19 | venv 20 | *.sublime-workspace 21 | Watch 22 | _.py 23 | htmlcov 24 | .coverage 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | notifications: 4 | email: false 5 | env: 6 | global: 7 | - TZ=UTC 8 | python: 9 | - 2.7 10 | - pypy 11 | - 3.5 12 | - 3.6 13 | - 3.7 14 | - 3.8 15 | install: 16 | - pip install -r requirements.txt 17 | - pip install -r tests/requirements.txt 18 | script: 19 | nosetests --rednose --with-cov 20 | after_success: 21 | codecov 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: watch test 2 | 3 | open: 4 | subl --project timestring.sublime-project 5 | 6 | deploy: tag upload 7 | 8 | tag: 9 | git tag -a v$(shell python -c "import timestring;print timestring.version;") -m "" 10 | git push origin v$(shell python -c "import timestring;print timestring.version;") 11 | 12 | upload: 13 | python setup.py sdist upload 14 | 15 | compare: 16 | hub compare $(shell git tag | tail -1)...master 17 | 18 | test: 19 | . venv/bin/activate; pip uninstall -y timestring 20 | . venv/bin/activate; python setup.py install 21 | . venv/bin/activate; nosetests --rednose --with-cov --cov-config=.coveragerc 22 | 23 | test3: 24 | . venv/bin/activate; pip3 uninstall -y timestring 25 | . venv/bin/activate; python3.3 setup.py install 26 | . venv/bin/activate; python3.3 -m tests.tests 27 | 28 | venv: 29 | virtualenv venv 30 | . venv/bin/activate; pip install -r requirements.txt 31 | . venv/bin/activate; pip install -r tests/requirements.txt 32 | . venv/bin/activate; python setup.py install 33 | 34 | venv3: 35 | . venv/bin/activate; pip3 install -r requirements.txt 36 | . venv/bin/activate; python3.3 setup.py install 37 | 38 | watch: 39 | watchr Watch 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timestring [![Build Status](https://secure.travis-ci.org/codecov/timestring.png)](http://travis-ci.org/stevepeak/timestring) [![Version](https://img.shields.io/pypi/v/timestring.svg)](https://github.com/stevepeak/timestring) [![codecov.io](https://codecov.io/github/stevepeak/timestring/coverage.svg?branch=master)](https://codecov.io/gh/codecov/timestring) 2 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcodecov%2Ftimestring.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcodecov%2Ftimestring?ref=badge_shield) 3 | 4 | Converting strings into usable time objects. The time objects, known as `Date` and `Range` have a number of methods that allow 5 | you to easily change and manage your users input dynamically. 6 | 7 | ## Install 8 | `pip install timestring` 9 | 10 | 11 | ## Ranges 12 | 13 | Ranges are simply two Dates. The first date, `Range().start` and `Range().end` represent just that, a start and end to a period of time. 14 | There are a couple reference points for Ranges. 15 | 16 | #### References 17 | * **no reference** => `x[ - - - - ]` 18 | - Adds the time to today. `Range('1 week')` would be `today + 7 days` 19 | * `this` => `[ - - x - - ]` 20 | - `this month` is from start of month to end of month. Therefore today **is** included. 21 | - ```Range("today") in Range("this month") == True``` 22 | * `next` => `x [ - - - - ]` 23 | - `next 3 weeks` takes today and finds the start of next weeks and continues to contain 3 weeks. 24 | - `Range("today") in Range("next 5 days") == False` and `Range("tomorrow") in Range("next 5 days") == True` 25 | * `ago` => `[ - - - - ] x` 26 | - same as `next` but in the past 27 | * `last` => `[ - - - - x ]` 28 | - `last 6 days` takes all of Today and encapsulates the last 6 days 29 | - ```Range("today") in Range("last 6 days") == True``` 30 | - empty reference ex `10 days` 31 | 32 | #### Samples 33 | The examples below all work with the following terms `minute`, `hour`, `day`, `month` and `year` work for the examples below. fyi `Today is 5/14/2013` 34 | 35 | > `this` will look at the references in its entirety 36 | ```python 37 | >>> Range('this year') 38 | From 01/01/13 00:00:00 to 01/01/14 00:00:00 39 | ``` 40 | 41 | *Notice how this year is from jan 1s to jan 1st of next year* The full year, all 12 months, is **this year** 42 | 43 | 44 | > `ago` and `last` will reference in the past 45 | ```python 46 | >>> Range('1 year ago') 47 | From 01/01/11 00:00:00 to 01/01/12 00:00:00 48 | ``` 49 | `1 year ago` is equivalent to `year ago`, and `last year` 50 | 51 | *Note* you add more years like this `5 years ago` which will be `From 01/01/07 00:00:00 to 01/01/08 00:00:00` 52 | 53 | ### See examples see the [test file](https://github.com/stevepeak/timestring/blob/master/tests/tests.py) 54 | 55 | More examples / documentation coming soon. 56 | 57 | ## License 58 | **timestring** is licensed under the Apache Licence, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html). 59 | 60 | 61 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcodecov%2Ftimestring.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcodecov%2Ftimestring?ref=badge_large) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytz>=2013b 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | version = '1.6.4' 5 | classifiers = ["Development Status :: 5 - Production/Stable", 6 | "License :: OSI Approved :: Apache Software License", 7 | "Programming Language :: Python", 8 | "Programming Language :: Python :: 2.7", 9 | "Programming Language :: Python :: 3.3", 10 | "Programming Language :: Python :: 3.4", 11 | "Programming Language :: Python :: 3.5", 12 | "Programming Language :: Python :: 3.6", 13 | "Programming Language :: Python :: Implementation :: PyPy"] 14 | 15 | setup(name='timestring', 16 | version=version, 17 | description="Human expressed time to Dates and Ranges", 18 | long_description="""Converting strings of into representable time via Date and Range objects. 19 | Plus features to compare and adjust Dates and Ranges.""", 20 | classifiers=classifiers, 21 | keywords='date time range datetime datestring', 22 | author='@stevepeak', 23 | author_email='hello@codecov.io', 24 | url='http://github.com/codecov/timestring', 25 | license='http://www.apache.org/licenses/LICENSE-2.0', 26 | packages=['timestring'], 27 | include_package_data=True, 28 | zip_safe=True, 29 | install_requires=["pytz>=2013b"], 30 | entry_points={'console_scripts': ['timestring=timestring:main']}) 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecov/timestring/d37ceacc5954dff3b5bd2f887936a98a668dda42/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | rednose 3 | nose-cov 4 | codecov 5 | ddt 6 | six 7 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import unittest 4 | from ddt import ddt, data 5 | from six import u 6 | from datetime import datetime, timedelta 7 | 8 | from timestring import Date 9 | from timestring import Range 10 | from timestring import parse 11 | from timestring.text2num import text2num 12 | 13 | 14 | @ddt 15 | class timestringTests(unittest.TestCase): 16 | def test_fullstring(self): 17 | now = datetime.now() 18 | 19 | # 20 | # DATE 21 | # 22 | date = Date("01/10/2015 at 7:30pm") 23 | self.assertEqual(date.year, 2015) 24 | self.assertEqual(date.month, 1) 25 | self.assertEqual(date.day, 10) 26 | self.assertEqual(date.hour, 19) 27 | self.assertEqual(date.minute, 30) 28 | 29 | date = Date("may 23rd, 1988 at 6:24 am") 30 | self.assertEqual(date.year, 1988) 31 | self.assertEqual(date.month, 5) 32 | self.assertEqual(date.day, 23) 33 | self.assertEqual(date.hour, 6) 34 | self.assertEqual(date.minute, 24) 35 | 36 | # 37 | # RANGE 38 | # 39 | r = Range('From 04/17/13 04:18:00 to 05/01/13 17:01:00', tz='US/Central') 40 | self.assertEqual(r.start.year, 2013) 41 | self.assertEqual(r.start.month, 4) 42 | self.assertEqual(r.start.day, 17) 43 | self.assertEqual(r.start.hour, 4) 44 | self.assertEqual(r.start.minute, 18) 45 | self.assertEqual(r.end.year, 2013) 46 | self.assertEqual(r.end.month, 5) 47 | self.assertEqual(r.end.day, 1) 48 | self.assertEqual(r.end.hour, 17) 49 | self.assertEqual(r.end.minute, 1) 50 | 51 | _range = Range("between january 15th at 3 am and august 5th 5pm") 52 | self.assertEqual(_range[0].year, now.year) 53 | self.assertEqual(_range[0].month, 1) 54 | self.assertEqual(_range[0].day, 15) 55 | self.assertEqual(_range[0].hour, 3) 56 | self.assertEqual(_range[1].year, now.year) 57 | self.assertEqual(_range[1].month, 8) 58 | self.assertEqual(_range[1].day, 5) 59 | self.assertEqual(_range[1].hour, 17) 60 | 61 | _range = Range("2012 feb 2 1:13PM to 6:41 am on sept 8 2012") 62 | self.assertEqual(_range[0].year, 2012) 63 | self.assertEqual(_range[0].month, 2) 64 | self.assertEqual(_range[0].day, 2) 65 | self.assertEqual(_range[0].hour, 13) 66 | self.assertEqual(_range[0].minute, 13) 67 | self.assertEqual(_range[1].year, 2012) 68 | self.assertEqual(_range[1].month, 9) 69 | self.assertEqual(_range[1].day, 8) 70 | self.assertEqual(_range[1].hour, 6) 71 | self.assertEqual(_range[1].minute, 41) 72 | 73 | date = Date('2013-09-10T10:45:50') 74 | self.assertEqual(date.year, 2013) 75 | self.assertEqual(date.month, 9) 76 | self.assertEqual(date.day, 10) 77 | self.assertEqual(date.hour, 10) 78 | self.assertEqual(date.minute, 45) 79 | self.assertEqual(date.second, 50) 80 | 81 | _range = Range('tomorrow 10am to 5pm') 82 | tomorrow = now + timedelta(days=1) 83 | self.assertEquals(_range.start.year, tomorrow.year) 84 | self.assertEquals(_range.end.year, tomorrow.year) 85 | self.assertEquals(_range.start.month, tomorrow.month) 86 | self.assertEquals(_range.end.month, tomorrow.month) 87 | self.assertEquals(_range.start.day, tomorrow.day) 88 | self.assertEquals(_range.end.day, tomorrow.day) 89 | self.assertEquals(_range.start.hour, 10) 90 | self.assertEquals(_range.end.hour, 17) 91 | 92 | def test_dates(self): 93 | date = Date("August 25th, 2014 12:30 PM") 94 | [self.assertEqual(*m) for m in ((date.year, 2014), (date.month, 8), (date.day, 25), (date.hour, 12), (date.minute, 30), (date.second, 0))] 95 | 96 | date = Date("may 23, 2018 1 pm") 97 | [self.assertEqual(*m) for m in ((date.year, 2018), (date.month, 5), (date.day, 23), (date.hour, 13), (date.minute, 0), (date.second, 0))] 98 | 99 | date = Date("1-2-13 2 am") 100 | [self.assertEqual(*m) for m in ((date.year, 2013), (date.month, 1), (date.day, 2), (date.hour, 2), (date.minute, 0), (date.second, 0))] 101 | 102 | date = Date("dec 15th '01 at 6:25:01 am") 103 | [self.assertEqual(*m) for m in ((date.year, 2001), (date.month, 12), (date.day, 15), (date.hour, 6), (date.minute, 25), (date.second, 1))] 104 | 105 | def test_singles(self): 106 | now = datetime.now() 107 | # 108 | # Single check 109 | # 110 | self.assertEqual(Date("2012").year, 2012) 111 | self.assertEqual(Date("January 2013").month, 1) 112 | self.assertEqual(Date("feb 2011").month, 2) 113 | self.assertEqual(Date("05/23/2012").month, 5) 114 | self.assertEqual(Date("01/10/2015 at 7:30pm").month, 1) 115 | self.assertEqual(Date("today").day, now.day) 116 | self.assertEqual(Range('january')[0].month, 1) 117 | self.assertEqual(Range('january')[0].day, 1) 118 | self.assertEqual(Range('january')[0].hour, 0) 119 | self.assertEqual(Range('january')[1].month, 2) 120 | self.assertEqual(Range('january')[1].day, 1) 121 | self.assertEqual(Range('january')[1].hour, 0) 122 | self.assertEqual(Range('2010')[0].year, 2010) 123 | self.assertEqual(Range('2010')[0].month, 1) 124 | self.assertEqual(Range('2010')[0].day, 1) 125 | self.assertEqual(Range('2010')[0].hour, 0) 126 | self.assertEqual(Range('2010')[1].year, 2011) 127 | self.assertEqual(Range('2010')[1].month, 1) 128 | self.assertEqual(Range('2010')[1].day, 1) 129 | self.assertEqual(Range('2010')[1].hour, 0) 130 | 131 | self.assertEqual(Range('january 2011')[0].year, 2011) 132 | self.assertEqual(Range('january 2011')[0].month, 1) 133 | self.assertEqual(Range('january 2011')[0].day, 1) 134 | self.assertEqual(Range('january 2011')[0].hour, 0) 135 | self.assertEqual(Range('january 2011')[1].year, 2011) 136 | self.assertEqual(Range('january 2011')[1].month, 2) 137 | self.assertEqual(Range('january 2011')[1].day, 1) 138 | self.assertEqual(Range('january 2011')[1].hour, 0) 139 | 140 | self.assertEqual(Date(1374681560).year, 2013) 141 | self.assertEqual(Date(1374681560).month, 7) 142 | self.assertEqual(Date(1374681560).day, 24) 143 | self.assertEqual(Date(str(1374681560)).year, 2013) 144 | self.assertEqual(Date(str(1374681560)).month, 7) 145 | self.assertEqual(Date(str(1374681560)).day, 24) 146 | 147 | self.assertEqual(Range(1374681560).start.day, 24) 148 | self.assertEqual(Range(1374681560).end.day, 25) 149 | 150 | # offset timezones 151 | self.assertEqual(Date("2014-03-06 15:33:43.764419-05").hour, 20) 152 | 153 | def test_this(self): 154 | now = datetime.now() 155 | # 156 | # this year 157 | # 158 | year = Range('this year') 159 | self.assertEqual(year.start.year, now.year) 160 | self.assertEqual(year.start.month, 1) 161 | self.assertEqual(year.start.day, 1) 162 | self.assertEqual(year.start.hour, 0) 163 | self.assertEqual(year.start.minute, 0) 164 | self.assertEqual(year.end.year, now.year+1) 165 | self.assertEqual(year.end.month, 1) 166 | self.assertEqual(year.end.day, 1) 167 | self.assertEqual(year.end.hour, 0) 168 | self.assertEqual(year.end.minute, 0) 169 | 170 | # 171 | # 1 year (from now) 172 | # 173 | year = Range('1 year') 174 | self.assertEqual(year.start.year, (now + timedelta(days=1)).year-1) 175 | self.assertEqual(year.start.month, (now + timedelta(days=1)).month) 176 | self.assertEqual(year.start.day, (now + timedelta(days=1)).day) 177 | self.assertEqual(year.start.hour, 0) 178 | self.assertEqual(year.start.minute, 0) 179 | self.assertEqual(year.end.year, (now + timedelta(days=1)).year) 180 | self.assertEqual(year.end.month, (now + timedelta(days=1)).month) 181 | self.assertEqual(year.end.day, (now + timedelta(days=1)).day) 182 | self.assertEqual(year.end.hour, 0) 183 | self.assertEqual(year.end.minute, 0) 184 | 185 | # 186 | # this month 187 | # 188 | month = Range('this month') 189 | self.assertEqual(month.start.year, now.year) 190 | self.assertEqual(month.start.month, now.month) 191 | self.assertEqual(month.start.day, 1) 192 | self.assertEqual(month.start.hour, 0) 193 | self.assertEqual(month.start.minute, 0) 194 | self.assertEqual(month.end.year, month.start.year + (1 if month.start.month+1 == 13 else 0)) 195 | self.assertEqual(month.end.month, (month.start.month + 1) if month.start.month+1 < 13 else 1) 196 | self.assertEqual(month.end.day, 1) 197 | self.assertEqual(month.end.hour, 0) 198 | self.assertEqual(month.end.minute, 0) 199 | 200 | # 201 | # this month w/ offset 202 | # 203 | mo = Range('this month', offset=dict(hour=6)) 204 | self.assertEqual(mo.start.year, now.year) 205 | self.assertEqual(mo.start.month, now.month) 206 | self.assertEqual(mo.start.day, 1) 207 | self.assertEqual(mo.start.hour, 6) 208 | self.assertEqual(mo.start.minute, 0) 209 | self.assertEqual(mo.end.year, mo.start.year + (1 if mo.start.month+1 == 13 else 0)) 210 | self.assertEqual(mo.end.month, (mo.start.month + 1) if mo.start.month+1 < 13 else 1) 211 | self.assertEqual(mo.end.day, 1) 212 | self.assertEqual(mo.end.hour, 6) 213 | self.assertEqual(mo.end.minute, 0) 214 | 215 | self.assertEqual(len(Range('6d')), 518400) 216 | self.assertEqual(len(Range('6 d')), 518400) 217 | self.assertEqual(len(Range('6 days')), 518400) 218 | self.assertEqual(len(Range('12h')), 43200) 219 | self.assertEqual(len(Range('6 h')), 21600) 220 | self.assertEqual(len(Range('10m')), 600) 221 | self.assertEqual(len(Range('10 m')), 600) 222 | self.assertEqual(len(Range('10 s')), 10) 223 | self.assertEqual(len(Range('10s')), 10) 224 | 225 | def test_dow(self): 226 | # 227 | # DOW 228 | # 229 | for x, day in enumerate(('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday')): 230 | d, r = Date(day), Range(day) 231 | self.assertEqual(d.hour, 0) 232 | self.assertEqual(d.weekday, 1 + x) 233 | # length is 1 day in seconds 234 | self.assertEqual(len(r), 86400) 235 | self.assertEqual(r.start.hour, 0) 236 | self.assertEqual(r.end.hour, 0) 237 | self.assertEqual(r.end.weekday, 1 if x+1 == 7 else (2+x)) 238 | 239 | def test_offset(self): 240 | now = datetime.now() 241 | 242 | # 243 | # Offset 244 | # 245 | self.assertEqual(Date("today", offset=dict(hour=6)).hour, 6) 246 | self.assertEqual(Date("today", offset=dict(hour=6)).day, now.day) 247 | self.assertEqual(Range("this week", offset=dict(hour=10)).start.hour, 10) 248 | self.assertEqual(Date("yesterday", offset=dict(hour=10)).hour, 10) 249 | self.assertEqual(Date("august 25th 7:30am", offset=dict(hour=10)).hour, 7) 250 | 251 | def test_lengths(self): 252 | # 253 | # Lengths 254 | # 255 | self.assertEqual(len(Range("next 10 weeks")), 5443200) 256 | self.assertEqual(len(Range("this week")), 604800) 257 | self.assertEqual(len(Range("3 weeks")), 1814400) 258 | self.assertEqual(len(Range('yesterday')), 86400) 259 | 260 | def test_in(self): 261 | # 262 | # in 263 | # 264 | self.assertTrue(Date('yesterday') in Range("last 7 days")) 265 | self.assertTrue(Date('today') in Range('this month')) 266 | self.assertTrue(Date('today') in Range('this month')) 267 | self.assertTrue(Range('this month') in Range('this year')) 268 | self.assertTrue(Range('this day') in Range('this week')) 269 | # these might not always be true because of end of week 270 | # self.assertTrue(Range('this week') in Range('this month')) 271 | # self.assertTrue(Range('this week') in Range('this year')) 272 | 273 | def test_tz(self): 274 | # 275 | # TZ 276 | # 277 | self.assertEqual(Date('today', tz="US/Central").tz.zone, 'US/Central') 278 | 279 | def test_cut(self): 280 | # 281 | # Cut 282 | # 283 | self.assertTrue(Range('from january 10th 2010 to february 2nd 2010').cut('10 days') == Range('from january 10th 2010 to jan 20th 2010')) 284 | self.assertTrue(Date("jan 10") + '1 day' == Date("jan 11")) 285 | self.assertTrue(Date("jan 10") - '5 day' == Date("jan 5")) 286 | 287 | def test_compare(self): 288 | self.assertFalse(Range('10 days') == Date('yestserday')) 289 | self.assertTrue(Date('yestserday') in Range('10 days')) 290 | self.assertTrue(Range('10 days') in Range('100 days')) 291 | self.assertTrue(Range('next 2 weeks') > Range('1 year')) 292 | self.assertTrue(Range('yesterday') < Range('now')) 293 | 294 | def test_last(self): 295 | now = datetime.now() 296 | # 297 | # last year 298 | # 299 | year = Range('last year') 300 | self.assertEqual(year.start.year, now.year - 1) 301 | self.assertEqual(year.start.month, now.month) 302 | self.assertEqual(year.start.day, now.day) 303 | self.assertEqual(year.start.hour, 0) 304 | self.assertEqual(year.start.minute, 0) 305 | self.assertEqual(year.end.year, now.year) 306 | self.assertEqual(year.end.month, now.month) 307 | self.assertEqual(year.end.day, now.day) 308 | self.assertEqual(year.end.hour, 0) 309 | self.assertEqual(year.end.minute, 0) 310 | self.assertTrue(Date('today') in year) 311 | 312 | self.assertTrue(Date('last tuesday') in Range('8 days')) 313 | self.assertTrue(Date('monday') in Range('8 days')) 314 | self.assertTrue(Date('last fri') in Range('8 days')) 315 | self.assertEqual(Range('1 year ago'), Range('last year')) 316 | self.assertEqual(Range('year ago'), Range('last year')) 317 | 318 | def test_psql_infinity(self): 319 | d = Date('infinity') 320 | self.assertTrue(d > 'now') 321 | self.assertTrue(d > 'today') 322 | self.assertTrue(d > 'next week') 323 | 324 | self.assertFalse(d in Range('this year')) 325 | self.assertFalse(d in Range('next 5 years')) 326 | 327 | self.assertTrue(Range('month') < d) 328 | 329 | r = Range('today', 'infinity') 330 | 331 | self.assertTrue('next 5 years' in r) 332 | self.assertTrue(Date('today') in r) 333 | self.assertTrue(d in r) 334 | self.assertFalse(d > r) 335 | self.assertFalse(r > d) 336 | 337 | r = Range('["2013-12-09 06:57:46.54502-05",infinity)') 338 | self.assertTrue(r.end == 'infinity') 339 | self.assertTrue('next 5 years' in r) 340 | self.assertTrue(Date('today') in r) 341 | self.assertTrue(d in r) 342 | self.assertFalse(d > r) 343 | self.assertFalse(r > d) 344 | self.assertEqual(r.start.year, 2013) 345 | self.assertEqual(r.start.month, 12) 346 | self.assertEqual(r.start.day, 9) 347 | self.assertEqual(r.start.hour, 11) 348 | self.assertEqual(r.start.minute, 57) 349 | self.assertEqual(r.start.second, 46) 350 | 351 | def test_date_adjustment(self): 352 | d = Date("Jan 1st 2014 at 10 am") 353 | self.assertEqual(d.year, 2014) 354 | self.assertEqual(d.month, 1) 355 | self.assertEqual(d.day, 1) 356 | self.assertEqual(d.hour, 10) 357 | self.assertEqual(d.minute, 0) 358 | self.assertEqual(d.second, 0) 359 | 360 | d.hour = 5 361 | d.day = 15 362 | d.month = 4 363 | d.year = 2013 364 | d.minute = 40 365 | d.second = 14 366 | 367 | self.assertEqual(d.year, 2013) 368 | self.assertEqual(d.month, 4) 369 | self.assertEqual(d.day, 15) 370 | self.assertEqual(d.hour, 5) 371 | self.assertEqual(d.minute, 40) 372 | self.assertEqual(d.second, 14) 373 | 374 | self.assertEqual(str(d.date), "2013-04-15 05:40:14") 375 | 376 | def test_parse(self): 377 | self.assertEqual(parse('tuesday at 10pm')['hour'], 22) 378 | self.assertEqual(parse('tuesday at 10pm')['weekday'], 2) 379 | self.assertEqual(parse('may of 2014')['year'], 2014) 380 | 381 | @data((1, "one"), 382 | (12, "twelve"), 383 | (72, "seventy two"), 384 | (300, "three hundred"), 385 | (1200, "twelve hundred"), 386 | (12304, "twelve thousand three hundred four"), 387 | (6000000, "six million"), 388 | (6400005, "six million four hundred thousand five"), 389 | (123456789012, "one hundred twenty three billion four hundred fifty six million seven hundred eighty nine thousand twelve"), 390 | (4000000000000000000000000000000000, "four decillion")) 391 | def test_string_to_number(self, data): 392 | (equals, string) = data 393 | self.assertEqual(text2num(string), equals) 394 | self.assertEqual(text2num(u(string)), equals) 395 | 396 | def test_plus(self): 397 | date1 = Date("october 18, 2013 10:04:32 PM") 398 | date2 = date1 + "10 seconds" 399 | self.assertEqual(date1.second + 10, date2.second) 400 | 401 | 402 | def main(): 403 | os.environ['TZ'] = 'UTC' 404 | time.tzset() 405 | unittest.main() 406 | 407 | 408 | if __name__ == '__main__': 409 | main() 410 | -------------------------------------------------------------------------------- /timestring/Date.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import pytz 4 | from copy import copy 5 | from datetime import datetime, timedelta 6 | 7 | from timestring.text2num import text2num 8 | from timestring import TimestringInvalid 9 | from timestring.timestring_re import TIMESTRING_RE 10 | 11 | try: 12 | unicode 13 | except NameError: 14 | unicode = str 15 | long = int 16 | 17 | CLEAN_NUMBER = re.compile(r"[\D]") 18 | 19 | class Date(object): 20 | def __init__(self, date, offset=None, start_of_week=None, tz=None, verbose=False): 21 | if isinstance(date, Date): 22 | self.date = copy(date.date) 23 | return 24 | 25 | # The original request 26 | self._original = date 27 | if tz: 28 | tz = pytz.timezone(str(tz)) 29 | 30 | if date == 'infinity': 31 | self.date = 'infinity' 32 | 33 | elif date == 'now': 34 | self.date = datetime.now() 35 | 36 | elif type(date) in (str, unicode) and re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+-\d{2}", date): 37 | self.date = datetime.strptime(date[:-3], "%Y-%m-%d %H:%M:%S.%f") - timedelta(hours=int(date[-3:])) 38 | 39 | else: 40 | # Determinal starting date. 41 | if type(date) in (str, unicode): 42 | """The date is a string and needs to be converted into a for processesing 43 | """ 44 | _date = date.lower() 45 | res = TIMESTRING_RE.search(_date.strip()) 46 | if res: 47 | date = res.groupdict() 48 | if verbose: 49 | print("Matches:\n", ''.join(["\t%s: %s\n" % (k, v) for k, v in date.items() if v])) 50 | else: 51 | raise TimestringInvalid('Invalid date string >> %s' % date) 52 | 53 | date = dict((k, v if type(v) is str else v) for k, v in date.items() if v) 54 | #print(_date, dict(map(lambda a: (a, date.get(a)), filter(lambda a: date.get(a), date)))) 55 | 56 | if isinstance(date, dict): 57 | # Initial date. 58 | new_date = datetime(*time.localtime()[:3]) 59 | if tz and tz.zone != "UTC": 60 | # 61 | # The purpose here is to adjust what day it is based on the timezeone 62 | # 63 | ts = datetime.now() 64 | # Daylight savings === second Sunday in March and reverts to standard time on the first Sunday in November 65 | # Monday is 0 and Sunday is 6. 66 | # 14 days - dst_start.weekday() 67 | dst_start = datetime(ts.year, 3, 1, 2, 0, 0) + timedelta(13 - datetime(ts.year, 3, 1).weekday()) 68 | dst_end = datetime(ts.year, 11, 1, 2, 0, 0) + timedelta(6 - datetime(ts.year, 11, 1).weekday()) 69 | 70 | ts = ts + tz.utcoffset(new_date, is_dst=(dst_start < ts < dst_end)) 71 | new_date = datetime(ts.year, ts.month, ts.day) 72 | 73 | if date.get('unixtime'): 74 | new_date = datetime.fromtimestamp(int(date.get('unixtime'))) 75 | 76 | # !number of (days|...) (ago)? 77 | elif date.get('num') and (date.get('delta') or date.get('delta_2')): 78 | if date.get('num', '').find('couple') > -1: 79 | i = 2 * int(1 if date.get('ago', True) or date.get('ref') == 'last' else -1) 80 | else: 81 | i = int(text2num(date.get('num', 'one'))) * int(1 if date.get('ago') or (date.get('ref', '') or '') == 'last' else -1) 82 | 83 | delta = (date.get('delta') or date.get('delta_2')).lower() 84 | if delta.startswith('y'): 85 | try: 86 | new_date = new_date.replace(year=(new_date.year - i)) 87 | # day is out of range for month 88 | except ValueError: 89 | new_date = new_date - timedelta(days=(365*i)) 90 | elif delta.startswith('month'): 91 | try: 92 | new_date = new_date.replace(month=(new_date.month - i)) 93 | # day is out of range for month 94 | except ValueError: 95 | new_date = new_date - timedelta(days=(30*i)) 96 | 97 | elif delta.startswith('q'): 98 | ''' 99 | This section is not working... 100 | Most likely need a generator that will take me to the right quater. 101 | ''' 102 | q1, q2, q3, q4 = datetime(new_date.year, 1, 1), datetime(new_date.year, 4, 1), datetime(new_date.year, 7, 1), datetime(new_date.year, 10, 1) 103 | if q1 <= new_date < q2: 104 | # We are in Q1 105 | if i == -1: 106 | new_date = datetime(new_date.year-1, 10, 1) 107 | else: 108 | new_date = q2 109 | elif q2 <= new_date < q3: 110 | # We are in Q2 111 | pass 112 | elif q3 <= new_date < q4: 113 | # We are in Q3 114 | pass 115 | else: 116 | # We are in Q4 117 | pass 118 | new_date = new_date - timedelta(days=(91*i)) 119 | 120 | elif delta.startswith('w'): 121 | new_date = new_date - timedelta(days=(i * 7)) 122 | 123 | else: 124 | new_date = new_date - timedelta(**{('days' if delta.startswith('d') else 'hours' if delta.startswith('h') else 'minutes' if delta.startswith('m') else 'seconds'): i}) 125 | 126 | # !dow 127 | if [date.get(key) for key in ('day', 'day_2', 'day_3') if date.get(key)]: 128 | dow = max([date.get(key) for key in ('day', 'day_2', 'day_3') if date.get(key)]) 129 | iso = dict(monday=1, tuesday=2, wednesday=3, thursday=4, friday=5, saturday=6, sunday=7, mon=1, tue=2, tues=2, wed=3, wedn=3, thu=4, thur=4, fri=5, sat=6, sun=7).get(dow) 130 | if iso: 131 | # determin which direction 132 | if date.get('ref') not in ('this', 'next'): 133 | days = iso - new_date.isoweekday() - (7 if iso >= new_date.isoweekday() else 0) 134 | else: 135 | days = iso - new_date.isoweekday() + (7 if iso < new_date.isoweekday() else 0) 136 | 137 | new_date = new_date + timedelta(days=days) 138 | 139 | elif dow == 'yesterday': 140 | new_date = new_date - timedelta(days=1) 141 | elif dow == 'tomorrow': 142 | new_date = new_date + timedelta(days=1) 143 | 144 | # !year 145 | year = [int(CLEAN_NUMBER.sub('', date[key])) for key in ('year', 'year_2', 'year_3', 'year_4', 'year_5', 'year_6') if date.get(key)] 146 | if year: 147 | year = max(year) 148 | if len(str(year)) != 4: 149 | year += 2000 if year <= 40 else 1900 150 | try: 151 | new_date = new_date.replace(year=year) 152 | except ValueError: # leap year 153 | new_date = new_date.replace(year=year,day=28) 154 | 155 | # !month 156 | month = [date.get(key) for key in ('month', 'month_1', 'month_2', 'month_3', 'month_4') if date.get(key)] 157 | if month: 158 | new_date = new_date.replace(day=1) 159 | new_date = new_date.replace(month=int(max(month)) if re.match('^\d+$', max(month)) else dict(january=1, february=2, march=3, april=4, june=6, july=7, august=8, september=9, october=10, november=11, december=12, jan=1, feb=2, mar=3, apr=4, may=5, jun=6, jul=7, aug=8, sep=9, sept=9, oct=10, nov=11, dec=12).get(max(month), new_date.month)) 160 | 161 | # !day 162 | day = [date.get(key) for key in ('date', 'date_2', 'date_3') if date.get(key)] 163 | if day: 164 | new_date = new_date.replace(day=int(max(day))) 165 | 166 | # !daytime 167 | if date.get('daytime'): 168 | if date['daytime'].find('this time') >= 1: 169 | new_date = new_date.replace(hour=datetime(*time.localtime()[:5]).hour, 170 | minute=datetime(*time.localtime()[:5]).minute) 171 | else: 172 | new_date = new_date.replace(hour=dict(morning=9, noon=12, afternoon=15, evening=18, night=21, nighttime=21, midnight=24).get(date.get('daytime'), 12)) 173 | # No offset because the hour was set. 174 | offset = False 175 | 176 | # !hour 177 | hour = [date.get(key) for key in ('hour', 'hour_2', 'hour_3') if date.get(key)] 178 | if hour: 179 | new_date = new_date.replace(hour=int(max(hour))) 180 | am = [date.get(key) for key in ('am', 'am_1') if date.get(key)] 181 | if am and max(am) in ('p', 'pm'): 182 | h = int(max(hour)) 183 | if h < 12: 184 | new_date = new_date.replace(hour=h+12) 185 | # No offset because the hour was set. 186 | offset = False 187 | 188 | #minute 189 | minute = [date.get(key) for key in ('minute', 'minute_2') if date.get(key)] 190 | if minute: 191 | new_date = new_date.replace(minute=int(max(minute))) 192 | 193 | #second 194 | seconds = date.get('seconds', 0) 195 | if seconds: 196 | new_date = new_date.replace(second=int(seconds)) 197 | 198 | self.date = new_date 199 | 200 | elif type(date) in (int, long, float) and re.match('^\d{10}$', str(date)): 201 | self.date = datetime.fromtimestamp(int(date)) 202 | 203 | elif isinstance(date, datetime): 204 | self.date = date 205 | 206 | elif date is None: 207 | self.date = datetime.now() 208 | 209 | else: 210 | # Set to the current date Y, M, D, H0, M0, S0 211 | self.date = datetime(*time.localtime()[:3]) 212 | 213 | if tz: 214 | self.date = self.date.replace(tzinfo=tz) 215 | 216 | # end if type(date) is types.DictType: and self.date.hour == 0: 217 | if offset and isinstance(offset, dict): 218 | self.date = self.date.replace(**offset) 219 | 220 | def __repr__(self): 221 | return "" % (str(self), id(self)) 222 | 223 | @property 224 | def year(self): 225 | if self.date != 'infinity': 226 | return self.date.year 227 | 228 | @year.setter 229 | def year(self, year): 230 | self.date = self.date.replace(year=year) 231 | 232 | @property 233 | def month(self): 234 | if self.date != 'infinity': 235 | return self.date.month 236 | 237 | @month.setter 238 | def month(self, month): 239 | self.date = self.date.replace(month=month) 240 | 241 | @property 242 | def day(self): 243 | if self.date != 'infinity': 244 | return self.date.day 245 | 246 | @day.setter 247 | def day(self, day): 248 | self.date = self.date.replace(day=day) 249 | 250 | @property 251 | def hour(self): 252 | if self.date != 'infinity': 253 | return self.date.hour 254 | 255 | @hour.setter 256 | def hour(self, hour): 257 | self.date = self.date.replace(hour=hour) 258 | 259 | @property 260 | def minute(self): 261 | if self.date != 'infinity': 262 | return self.date.minute 263 | 264 | @minute.setter 265 | def minute(self, minute): 266 | self.date = self.date.replace(minute=minute) 267 | 268 | @property 269 | def second(self): 270 | if self.date != 'infinity': 271 | return self.date.second 272 | 273 | @second.setter 274 | def second(self, second): 275 | self.date = self.date.replace(second=second) 276 | 277 | @property 278 | def weekday(self): 279 | if self.date != 'infinity': 280 | return self.date.isoweekday() 281 | 282 | @property 283 | def tz(self): 284 | if self.date != 'infinity': 285 | return self.date.tzinfo 286 | 287 | @tz.setter 288 | def tz(self, tz): 289 | if self.date != 'infinity': 290 | if tz is None: 291 | self.date = self.date.replace(tzinfo=None) 292 | else: 293 | self.date = self.date.replace(tzinfo=pytz.timezone(tz)) 294 | 295 | def replace(self, **k): 296 | """Note returns a new Date obj""" 297 | if self.date != 'infinity': 298 | return Date(self.date.replace(**k)) 299 | else: 300 | return Date('infinity') 301 | 302 | def adjust(self, to): 303 | ''' 304 | Adjusts the time from kwargs to timedelta 305 | **Will change this object** 306 | 307 | return new copy of self 308 | ''' 309 | if self.date == 'infinity': 310 | return 311 | new = copy(self) 312 | if type(to) in (str, unicode): 313 | to = to.lower() 314 | res = TIMESTRING_RE.search(to) 315 | if res: 316 | rgroup = res.groupdict() 317 | if (rgroup.get('delta') or rgroup.get('delta_2')): 318 | i = int(text2num(rgroup.get('num', 'one'))) * (-1 if to.startswith('-') else 1) 319 | delta = (rgroup.get('delta') or rgroup.get('delta_2')).lower() 320 | if delta.startswith('y'): 321 | try: 322 | new.date = new.date.replace(year=(new.date.year + i)) 323 | except ValueError: 324 | # day is out of range for month 325 | new.date = new.date + timedelta(days=(365 * i)) 326 | elif delta.startswith('month'): 327 | if (new.date.month + i) > 12: 328 | new.date = new.date.replace(month=(i - (i / 12)), 329 | year=(new.date.year + 1 + (i / 12))) 330 | elif (new.date.month + i) < 1: 331 | new.date = new.date.replace(month=12, year=(new.date.year - 1)) 332 | else: 333 | new.date = new.date.replace(month=(new.date.month + i)) 334 | elif delta.startswith('q'): 335 | # NP 336 | pass 337 | elif delta.startswith('w'): 338 | new.date = new.date + timedelta(days=(7 * i)) 339 | elif delta.startswith('s'): 340 | new.date = new.date + timedelta(seconds=i) 341 | else: 342 | new.date = new.date + timedelta(**{('days' if delta.startswith('d') else 'hours' if delta.startswith('h') else 'minutes' if delta.startswith('m') else 'seconds'): i}) 343 | return new 344 | else: 345 | new.date = new.date + timedelta(seconds=int(to)) 346 | return new 347 | 348 | raise TimestringInvalid('Invalid addition request') 349 | 350 | def __nonzero__(self): 351 | return True 352 | 353 | def __add__(self, to): 354 | if self.date == 'infinity': 355 | return copy(self) 356 | return copy(self).adjust(to) 357 | 358 | def __sub__(self, to): 359 | if self.date == 'infinity': 360 | return copy(self) 361 | if type(to) in (str, unicode): 362 | to = to[1:] if to.startswith('-') else ('-'+to) 363 | elif type(to) in (int, float, long): 364 | to = to * -1 365 | return copy(self).adjust(to) 366 | 367 | def __format__(self, _): 368 | if self.date != 'infinity': 369 | return self.date.strftime('%x %X') 370 | else: 371 | return 'infinity' 372 | 373 | def __str__(self): 374 | """Returns date in representation of `%x %X` ie `2013-02-17 00:00:00`""" 375 | return str(self.date) 376 | 377 | def __gt__(self, other): 378 | if self.date == 'infinity': 379 | if isinstance(other, Date): 380 | return other.date != 'infinity' 381 | else: 382 | from .Range import Range 383 | if isinstance(other, Range): 384 | return other.end != 'infinity' 385 | return other != 'infinity' 386 | else: 387 | if isinstance(other, Date): 388 | if other.date == 'infinity': 389 | return False 390 | elif other.tz and self.tz is None: 391 | return self.date.replace(tzinfo=other.tz) > other.date 392 | elif self.tz and other.tz is None: 393 | return self.date > other.date.replace(tzinfo=self.tz) 394 | return self.date > other.date 395 | else: 396 | from .Range import Range 397 | if isinstance(other, Range): 398 | if other.end.date == 'infinity': 399 | return False 400 | if other.end.tz and self.tz is None: 401 | return self.date.replace(tzinfo=other.end.tz) > other.end.date 402 | elif self.tz and other.end.tz is None: 403 | return self.date > other.end.date.replace(tzinfo=self.tz) 404 | return self.date > other.end.date 405 | else: 406 | return self.__gt__(Date(other, tz=self.tz)) 407 | 408 | def __lt__(self, other): 409 | if self.date == 'infinity': 410 | # infinity can never by less then a date 411 | return False 412 | 413 | if isinstance(other, Date): 414 | if other.date == 'infinity': 415 | return True 416 | elif other.tz and self.tz is None: 417 | return self.date.replace(tzinfo=other.tz) < other.date 418 | elif self.tz and other.tz is None: 419 | return self.date < other.date.replace(tzinfo=self.tz) 420 | return self.date < other.date 421 | else: 422 | from .Range import Range 423 | if isinstance(other, Range): 424 | if other.end.tz and self.tz is None: 425 | return self.date.replace(tzinfo=other.end.tz) < other.end.date 426 | elif self.tz and other.end.tz is None: 427 | return self.date < other.end.date.replace(tzinfo=self.tz) 428 | return self.date < other.end.date 429 | else: 430 | return self.__lt__(Date(other, tz=self.tz)) 431 | 432 | def __ge__(self, other): 433 | return self > other or self == other 434 | 435 | def __le__(self, other): 436 | return self < other or self == other 437 | 438 | def __eq__(self, other): 439 | if isinstance(other, datetime): 440 | other = Date(other) 441 | if isinstance(other, Date): 442 | if other.date == 'infinity': 443 | return self.date == 'infinity' 444 | 445 | elif other.tz and self.tz is None: 446 | return self.date.replace(tzinfo=other.tz) == other.date 447 | 448 | elif self.tz and other.tz is None: 449 | return self.date == other.date.replace(tzinfo=self.tz) 450 | 451 | return self.date == other.date 452 | else: 453 | from .Range import Range 454 | if isinstance(other, Range): 455 | return False 456 | else: 457 | return self.__eq__(Date(other, tz=self.tz)) 458 | 459 | def __ne__(self, other): 460 | return not self.__eq__(other) 461 | 462 | def format(self, format_string='%x %X'): 463 | if self.date != 'infinity': 464 | return self.date.strftime(format_string) 465 | else: 466 | return 'infinity' 467 | 468 | def to_unixtime(self): 469 | if self.date != 'infinity': 470 | return time.mktime(self.date.timetuple()) 471 | else: 472 | return -1 473 | -------------------------------------------------------------------------------- /timestring/Range.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytz 3 | from copy import copy 4 | from datetime import datetime 5 | 6 | from timestring.Date import Date 7 | from timestring import TimestringInvalid 8 | from timestring.timestring_re import TIMESTRING_RE 9 | 10 | try: 11 | unicode 12 | except NameError: 13 | unicode = str 14 | long = int 15 | 16 | 17 | class Range(object): 18 | def __init__(self, start, end=None, offset=None, start_of_week=0, tz=None, verbose=False): 19 | """`start` can be type or 20 | """ 21 | self._dates = [] 22 | pgoffset = None 23 | 24 | if start is None: 25 | raise TimestringInvalid("Range object requires a start valie") 26 | 27 | if not isinstance(start, (Date, datetime)): 28 | start = str(start) 29 | if end and not isinstance(end, (Date, datetime)): 30 | end = str(end) 31 | 32 | if start and end: 33 | """start and end provided 34 | """ 35 | self._dates = (Date(start, tz=tz), Date(end, tz=tz)) 36 | 37 | elif start == 'infinity': 38 | # end was not provided 39 | self._dates = (Date('infinity'), Date('infinity')) 40 | 41 | elif re.search(r'(\s(and|to)\s)', start): 42 | """Both sides where provided in the start 43 | """ 44 | start = re.sub('^(between|from)\s', '', start.lower()) 45 | # Both arguments found in start variable 46 | r = tuple(re.split(r'(\s(and|to)\s)', start.strip())) 47 | self._dates = (Date(r[0], tz=tz), Date(r[-1], tz=tz)) 48 | 49 | elif re.match(r"(\[|\()((\"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?(\+|\-)\d{2}\")|infinity),((\"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?(\+|\-)\d{2}\")|infinity)(\]|\))", start): 50 | """postgresql tsrange and tstzranges support 51 | """ 52 | start, end = tuple(re.sub('[^\w\s\-\:\.\+\,]', '', start).split(',')) 53 | self._dates = (Date(start), Date(end)) 54 | 55 | else: 56 | now = datetime.now() 57 | # no tz info but offset provided, we are UTC so convert 58 | 59 | if re.search(r"(\+|\-)\d{2}$", start): 60 | # postgresql tsrange and tstzranges 61 | pgoffset = re.search(r"(\+|\-)\d{2}$", start).group() + " hours" 62 | 63 | # tz info provided 64 | if tz: 65 | now = now.replace(tzinfo=pytz.timezone(str(tz))) 66 | 67 | # Parse 68 | res = TIMESTRING_RE.search(start) 69 | if res: 70 | group = res.groupdict() 71 | if verbose: 72 | print(dict(map(lambda a: (a, group.get(a)), filter(lambda a: group.get(a), group)))) 73 | if (group.get('delta') or group.get('delta_2')) is not None: 74 | delta = (group.get('delta') or group.get('delta_2')).lower() 75 | 76 | # always start w/ today 77 | start = Date("today", offset=offset, tz=tz) 78 | 79 | # make delta 80 | di = "%s %s" % (str(int(group['num'] or 1)), delta) 81 | 82 | # this [ x ] 83 | if group['ref'] == 'this': 84 | 85 | if delta.startswith('y'): 86 | start = Date(datetime(now.year, 1, 1), offset=offset, tz=tz) 87 | 88 | # month 89 | elif delta.startswith('month'): 90 | start = Date(datetime(now.year, now.month, 1), offset=offset, tz=tz) 91 | 92 | # week 93 | elif delta.startswith('w'): 94 | start = Date("today", offset=offset, tz=tz) - (str(Date("today", tz=tz).date.weekday())+' days') 95 | 96 | # day 97 | elif delta.startswith('d'): 98 | start = Date("today", offset=offset, tz=tz) 99 | 100 | # hour 101 | elif delta.startswith('h'): 102 | start = Date("today", offset=dict(hour=now.hour+1), tz=tz) 103 | 104 | # minute, second 105 | elif delta.startswith('m') or delta.startswith('s'): 106 | start = Date("now", tz=tz) 107 | 108 | else: 109 | raise TimestringInvalid("Not a valid time reference") 110 | 111 | end = start + di 112 | 113 | #next x [ ] 114 | elif group['ref'] == 'next': 115 | if int(group['num'] or 1) > 1: 116 | di = "%s %s" % (str(int(group['num'] or 1) - 1), delta) 117 | end = start + di 118 | 119 | # ago [ ] x 120 | elif group.get('ago') or group['ref'] == 'last' and int(group['num'] or 1) == 1: 121 | #if group['ref'] == 'last' and int(group['num'] or 1) == 1: 122 | # start = start - ('1 ' + delta) 123 | end = start - di 124 | 125 | # last & no ref [ x] 126 | else: 127 | # need to include today with this reference 128 | if not (delta.startswith('h') or delta.startswith('m') or delta.startswith('s')): 129 | start = Range('today', offset=offset, tz=tz).end 130 | end = start - di 131 | 132 | elif group.get('month_1'): 133 | # a single month of this yeear 134 | start = Date(start, offset=offset, tz=tz) 135 | start = start.replace(day=1) 136 | end = start + '1 month' 137 | 138 | elif group.get('year_5'): 139 | # a whole year 140 | start = Date(start, offset=offset, tz=tz) 141 | start = start.replace(day=1, month=1) 142 | end = start + '1 year' 143 | 144 | else: 145 | # after all else, we set the end to + 1 day 146 | start = Date(start, offset=offset, tz=tz) 147 | end = start + '1 day' 148 | 149 | else: 150 | raise TimestringInvalid("Invalid timestring request") 151 | 152 | 153 | if end is None: 154 | # no end provided, so assume 24 hours 155 | end = start + '24 hours' 156 | 157 | if start > end: 158 | # flip them if this is so 159 | start, end = copy(end), copy(start) 160 | 161 | if pgoffset: 162 | start = start - pgoffset 163 | if end != 'infinity': 164 | end = end - pgoffset 165 | 166 | self._dates = (start, end) 167 | 168 | if self._dates[0] > self._dates[1]: 169 | self._dates = (self._dates[0], self._dates[1] + '1 day') 170 | 171 | def __repr__(self): 172 | return "" % (str(self), id(self)) 173 | 174 | def __getitem__(self, index): 175 | return self._dates[index] 176 | 177 | def __str__(self): 178 | return self.format() 179 | 180 | def __nonzero__(self): 181 | # Ranges are natuarally always true in statments link: if Range 182 | return True 183 | 184 | def format(self, format_string='%x %X'): 185 | return "From %s to %s" % (self[0].format(format_string) if isinstance(self[0], Date) else str(self[0]), 186 | self[1].format(format_string) if isinstance(self[1], Date) else str(self[1])) 187 | 188 | @property 189 | def start(self): 190 | return self[0] 191 | 192 | @property 193 | def end(self): 194 | return self[1] 195 | 196 | @property 197 | def elapse(self, short=False, format=True, min=None, round=None): 198 | if self.start == 'infinity' or self.end == 'infinity': 199 | return "infinity" 200 | # years, months, days, hours, minutes, seconds 201 | full = [0, 0, 0, 0, 0, 0] 202 | elapse = self[1].date - self[0].date 203 | days = elapse.days 204 | if days > 365: 205 | years = days / 365 206 | full[0] = years 207 | days = elapse.days - (years*365) 208 | if days > 30: 209 | months = days / 30 210 | full[1] = months 211 | days = days - (days / 30) 212 | 213 | full[2] = days 214 | 215 | full[3], full[4], full[5] = tuple(map(int, map(float, str(elapse).split(', ')[-1].split(':')))) 216 | 217 | if round: 218 | r = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] 219 | assert round in r[:-1], "round value is not allowed. Must be in "+",".join(r) 220 | if full[r.index(round)+1] > dict(months=6, days=15, hours=12, minutes=30, seconds=30).get(r[r.index(round)+1]): 221 | full[r.index(round)] += 1 222 | 223 | min = r[r.index(round)+1] 224 | 225 | if min: 226 | m = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] 227 | assert min in m, "min value is not allowed. Must be in "+",".join(m) 228 | for x in range(6-m.index(min)): 229 | full[5-x] = 0 230 | 231 | if format: 232 | if short: 233 | return re.sub('((? -1 268 | * {---[---} ] => 1 269 | * [---] {---} => -1 270 | * [---] same as {---} => 0 271 | * [--{-}--] => -1 272 | """ 273 | if isinstance(other, Range): 274 | # other has tz, I dont, so replace the tz 275 | start = self.start.replace(tzinfo=other.start.tz) if other.start.tz and self.start.tz is None else self.start 276 | end = self.end.replace(tzinfo=other.end.tz) if other.end.tz and self.end.tz is None else self.end 277 | 278 | if start == other.start and end == other.end: 279 | return 0 280 | elif start < other.start: 281 | return -1 282 | else: 283 | return 1 284 | 285 | elif isinstance(other, Date): 286 | if other.tz and self.start.tz is None: 287 | return 0 if other == self.start.replace(tzinfo=other.tz) else -1 if other > self.start.replace(tzinfo=other.start.tz) else 1 288 | return 0 if other == self.start else -1 if other > self.start else 1 289 | else: 290 | return self.cmp(Range(other, tz=self.start.tz)) 291 | 292 | def __contains__(self, other): 293 | """*Note: checks Range.start() only* 294 | Key: self = [], other = {} 295 | * [---{-}---] => True else False 296 | """ 297 | if isinstance(other, Date): 298 | 299 | # ~ .... | 300 | if self.start == 'infinity' and self.end >= other: 301 | return True 302 | 303 | # | .... ~ 304 | elif self.end == 'infinity' and self.start <= other: 305 | return True 306 | 307 | elif other == 'infinity': 308 | # infinitys cannot be contained, unless I'm infinity 309 | return self.start == 'infinity' or self.end == 'infinity' 310 | 311 | elif other.tz and self.start.tz is None: 312 | # we can safely update tzinfo 313 | return self.start.replace(tzinfo=other.tz).to_unixtime() <= other.to_unixtime() <= self.end.replace(tzinfo=other.tz).to_unixtime() 314 | 315 | return self.start <= other <= self.end 316 | 317 | elif isinstance(other, Range): 318 | # ~ .... | 319 | if self.start == 'infinity': 320 | # ~ <-- | 321 | return other.end <= self.end 322 | 323 | # | .... ~ 324 | elif self.end == 'infinity': 325 | # | --> ~ 326 | return self.start <= other.start 327 | 328 | elif other.start.tz and self.start.tz is None: 329 | return self.start.replace(tzinfo=other.start.tz).to_unixtime() <= other.start.to_unixtime() <= self.end.replace(tzinfo=other.start.tz).to_unixtime() \ 330 | and self.start.replace(tzinfo=other.start.tz).to_unixtime() <= other.end.to_unixtime() <= self.end.replace(tzinfo=other.start.tz).to_unixtime() 331 | 332 | return self.start <= other.start <= self.end and self.start <= other.end <= self.end 333 | 334 | else: 335 | return self.__contains__(Range(other, tz=self.start.tz)) 336 | 337 | def cut(self, by, from_start=True): 338 | """ Cuts this object from_start to the number requestd 339 | returns new instance 340 | """ 341 | s, e = copy(self.start), copy(self.end) 342 | if from_start: 343 | e = s + by 344 | else: 345 | s = e - by 346 | return Range(s, e) 347 | 348 | def adjust(self, to): 349 | # return a new instane, like datetime does 350 | return Range(self.start.adjust(to), 351 | self.end.adjust(to), tz=self.start.tz) 352 | 353 | def next(self, times=1): 354 | """Returns a new instance of self 355 | times is not supported yet. 356 | """ 357 | return Range(copy(self.end), 358 | self.end + self.elapse, tz=self.start.tz) 359 | 360 | def prev(self, times=1): 361 | """Returns a new instance of self 362 | times is not supported yet. 363 | """ 364 | return Range(self.start - self.elapse, 365 | copy(self.start), tz=self.start.tz) 366 | 367 | def __add__(self, to): 368 | return self.adjust(to) 369 | 370 | def __sub__(self, to): 371 | if type(to) in (str, unicode): 372 | to = to[1:] if to.startswith('-') else ('-'+to) 373 | elif type(to) in (int, long, float): 374 | to = to * -1 375 | return self.adjust(to) 376 | -------------------------------------------------------------------------------- /timestring/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import argparse 4 | from datetime import datetime 5 | 6 | __version__ = VERSION = version = '1.6.2' 7 | 8 | 9 | class TimestringInvalid(Exception): 10 | def __init__(self, reason): 11 | self.reason = reason 12 | 13 | def __str__(self): 14 | return self.reason 15 | 16 | 17 | from .Date import Date 18 | from .Range import Range 19 | from .timestring_re import TIMESTRING_RE 20 | 21 | 22 | try: 23 | """Register psycopg2 Adapters 24 | 25 | if psycopg2 is installed then automatically register 26 | the adapters for Date and Range. 27 | 28 | >>> db.mogrify("insert into my_table (range) values (%s);", 29 | timestring.Range("next week")) 30 | "insert into my_table (range) values (tstzrange('2014-03-03 00:00:00'::timestamptz, '2014-03-10 00:00:00'::timestamptz));" 31 | """ 32 | from psycopg2.extensions import register_adapter 33 | from psycopg2.extensions import AsIs 34 | 35 | def adapt_date(date): 36 | if date.tz: 37 | return AsIs("'%s'::timestamptz" % str(date.date)) 38 | else: 39 | return AsIs("'%s'::timestamp" % str(date.date)) 40 | 41 | def adapt_range(_range): 42 | if _range.start.tz: 43 | return AsIs("tstzrange('%s', '%s')" % (str(_range.start.date), str(_range.end.date))) 44 | else: 45 | return AsIs("tsrange('%s', '%s')" % (str(_range.start.date), str(_range.end.date))) 46 | 47 | register_adapter(Date, adapt_date) 48 | register_adapter(Range, adapt_range) 49 | 50 | except ImportError: 51 | pass 52 | 53 | 54 | def findall(text): 55 | """Find all the timestrings within a block of text. 56 | 57 | >>> timestring.findall("once upon a time, about 3 weeks ago, there was a boy whom was born on august 15th at 7:20 am. epic.") 58 | [ 59 | ('3 weeks ago,', ), 60 | ('august 15th at 7:20 am', ) 61 | ] 62 | """ 63 | results = TIMESTRING_RE.findall(text) 64 | dates = [] 65 | for date in results: 66 | if re.compile('((next|last)\s(\d+|couple(\sof))\s(weeks|months|quarters|years))|(between|from)', re.I).match(date[0]): 67 | dates.append((date[0].strip(), Range(date[0]))) 68 | else: 69 | dates.append((date[0].strip(), Date(date[0]))) 70 | return dates 71 | 72 | 73 | def parse(string): 74 | try: 75 | matches = TIMESTRING_RE.search(string).groupdict() 76 | date = Date(string) 77 | result = {} 78 | for k,v in matches.items(): 79 | if v: 80 | arg = k.split('_', 1)[0] 81 | if arg in ('year', 'month', 'day', 'hour', 'minute', 'second'): 82 | result.setdefault(arg, getattr(date, arg)) 83 | 84 | if result.get('day'): 85 | result['weekday'] = date.weekday 86 | 87 | return result 88 | except: 89 | return None 90 | 91 | 92 | 93 | def now(): 94 | return Date(datetime.now()) 95 | 96 | 97 | def main(): 98 | parser = argparse.ArgumentParser(prog='timestring', 99 | add_help=True, 100 | formatter_class=argparse.RawDescriptionHelpFormatter, 101 | epilog=""" """) 102 | parser.add_argument('--version', action='version', version="timestring v%s - http://github.com/stevepeak/timestring" % version) 103 | parser.add_argument('-d', '--date', action='store_true') 104 | parser.add_argument('--verbose', '-v', action="store_true", help="Verbose mode") 105 | parser.add_argument('args', nargs="+", help="Time input") 106 | 107 | if len(sys.argv) == 1: 108 | parser.print_help() 109 | else: 110 | args = parser.parse_args() 111 | if args.date: 112 | print(Date(" ".join(args.args), verbose=args.verbose)) 113 | else: 114 | print(Range(" ".join(args.args), verbose=args.verbose)) 115 | 116 | if __name__ == '__main__': 117 | main() 118 | -------------------------------------------------------------------------------- /timestring/text2num.py: -------------------------------------------------------------------------------- 1 | # https://github.com/ghewgill/text2num/blob/master/text2num.py 2 | 3 | # This library is a simple implementation of a function to convert textual 4 | # numbers written in English into their integer representations. 5 | # 6 | # This code is open source according to the MIT License as follows. 7 | # 8 | # Copyright (c) 2008 Greg Hewgill 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | 28 | import re 29 | 30 | Small = { 31 | 'zero': 0, 32 | 'one': 1, 33 | 'two': 2, 34 | 'three': 3, 35 | 'four': 4, 36 | 'five': 5, 37 | 'six': 6, 38 | 'seven': 7, 39 | 'eight': 8, 40 | 'nine': 9, 41 | 'ten': 10, 42 | 'eleven': 11, 43 | 'twelve': 12, 44 | 'thirteen': 13, 45 | 'fourteen': 14, 46 | 'fifteen': 15, 47 | 'sixteen': 16, 48 | 'seventeen': 17, 49 | 'eighteen': 18, 50 | 'nineteen': 19, 51 | 'twenty': 20, 52 | 'thirty': 30, 53 | 'forty': 40, 54 | 'fifty': 50, 55 | 'sixty': 60, 56 | 'seventy': 70, 57 | 'eighty': 80, 58 | 'ninety': 90 59 | } 60 | 61 | Magnitude = { 62 | 'thousand': 1000, 63 | 'million': 1000000, 64 | 'billion': 1000000000, 65 | 'trillion': 1000000000000, 66 | 'quadrillion': 1000000000000000, 67 | 'quintillion': 1000000000000000000, 68 | 'sextillion': 1000000000000000000000, 69 | 'septillion': 1000000000000000000000000, 70 | 'octillion': 1000000000000000000000000000, 71 | 'nonillion': 1000000000000000000000000000000, 72 | 'decillion': 1000000000000000000000000000000000, 73 | } 74 | 75 | class NumberException(Exception): 76 | def __init__(self, msg): 77 | Exception.__init__(self, msg) 78 | 79 | def text2num(s): 80 | a = re.split(r"[\s-]+", s.strip()) 81 | n = 0 82 | g = 0 83 | for w in a: 84 | try: 85 | x = int(w) 86 | except: 87 | x = Small.get(w, None) 88 | finally: 89 | if x is not None: 90 | g += x 91 | elif w == "hundred": 92 | g *= 100 93 | else: 94 | x = Magnitude.get(w, None) 95 | if x is not None: 96 | n += g * x 97 | g = 0 98 | else: 99 | raise NumberException("Unknown number: "+w) 100 | return n + g 101 | -------------------------------------------------------------------------------- /timestring/timestring_re.py: -------------------------------------------------------------------------------- 1 | import re 2 | TIMESTRING_RE = re.compile(re.sub('[\t\n\s]', '', re.sub('(\(\?\#[^\)]+\))', '', r''' 3 | ( 4 | ((?Pbetween|from|before|after|\>=?|\<=?|greater\s+th(a|e)n(\s+a)?|less\s+th(a|e)n(\s+a)?)\s+)? 5 | ( 6 | (?P\d{10}) 7 | 8 | | 9 | 10 | ( 11 | ( 12 | ((?Pnext|last|prev(ious)?|this)\s+)? 13 | (?P
14 | (?# =-=-=-= Matches:: number-frame-ago?, "4 weeks", "sixty days ago" =-=-=-= ) 15 | ( 16 | (?P((\d+|couple(\s+of)?|one|two|twenty|twelve|three|thirty|thirteen|four(teen|ty)?|five|fif(teen|ty)|six(teen|ty)?|seven(teen|ty)?|eight(een|y)?|nine(teen|ty)?|ten|eleven|hundred)\s*)*) 17 | ( 18 | (?Pseconds?|minutes?|hours?|days?|weeks?|months?|quarters?|years?)| 19 | ((?[YyQqDdHhMmSs])(?!\w)) 20 | ) 21 | (\s+(?Pago))? 22 | ) 23 | 24 | | 25 | 26 | (?# =-=-=-= Matches Days =-=-=-= ) 27 | (?Pyesterday|today|now|tomorrow|mondays?|tuesdays?|wednesdays?|thursdays?|fridays?|saturdays?|sundays?|mon|tues?|wedn?|thur?|fri|sat|sun) 28 | 29 | | 30 | 31 | (?# =-=-=-= Matches Y-M-D, M-D-Y ex. "january 5, 2012", "january 5th, '12", "jan 5th 2012" =-=-=-= ) 32 | ( 33 | ((?P(([12][089]\d{2})|('\d{2})))?([\/\-\s]+)?) 34 | (?Pjanuary|february|march|april|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec)[\/\-\s] 35 | ((?P(\d{1,2})(?!\d))(th|nd|st|rd)?) 36 | (,?\s(?P([12][089]|')?\d{2}))? 37 | ) 38 | 39 | | 40 | 41 | (?# =-=-=-= Matches "2012/12/11", "2013-09-10T", "5/23/2012", "05/2012", "2012" =-=-=-= ) 42 | ( 43 | ((?P[12][089]\d{2})[/-](?P[01]?\d)([/-](?P[0-3]?\d))?)T? 44 | | 45 | ((?P[01]?\d)[/-](?P[0-3]?\d)[/-](?P(([12][089]\d{2})|(\d{2})))) 46 | | 47 | ((?P[01]?\d)[/-](?P([12][089]\d{2})|(\d{2}))) 48 | | 49 | (?P([12][089]\d{2})|('\d{2})) 50 | ) 51 | 52 | | 53 | 54 | (?# =-=-=-= Matches "01:20", "6:35 pm", "7am", "noon" =-=-=-= ) 55 | ( 56 | ((?P[012]?[0-9]):(?P[0-5]\d)\s*(?Pam|pm|p|a)) 57 | | 58 | ((?P[012]?[0-9]):(?P[0-5]\d)(:(?P[0-5]\d))?) 59 | | 60 | ((?P[012]?[0-9])\s*(?Pam|pm|p|a|o'?clock)) 61 | | 62 | (?P(after)?noon|morning|((around|about|near|by)\s+)?this\s+time|evening|(mid)?night(time)?) 63 | ) 64 | 65 | | 66 | 67 | (?Pjanuary|february|march|april|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec) 68 | ) 69 | ) 70 | (?# =-=-=-= Conjunctions =-=-=-= ) 71 | ,?(\s+(on|at|of|by|and|to|@))?\s* 72 | )+ 73 | ) 74 | ) 75 | ''')), re.I) 76 | --------------------------------------------------------------------------------