├── .gitignore ├── requirements.txt ├── moment ├── __init__.py ├── utils.py ├── api.py ├── parse.py ├── core.py └── date.py ├── setup.py ├── README.md └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | .tox/ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dateparser>=0.7 2 | pytz>=2018.9 3 | times>=0.7 4 | -------------------------------------------------------------------------------- /moment/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import * 2 | from .core import Moment 3 | -------------------------------------------------------------------------------- /moment/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Added for Python 3 compatibility. 3 | """ 4 | 5 | import sys 6 | 7 | 8 | STRING_TYPES = (basestring, ) if sys.version_info < (3, 0) else (str, ) 9 | 10 | 11 | def _iteritems(data): 12 | "For Python 3 support." 13 | if sys.version_info < (3, 0): 14 | return data.iteritems() 15 | else: 16 | return data.items() 17 | -------------------------------------------------------------------------------- /moment/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple API functionality. 3 | """ 4 | 5 | from .core import Moment 6 | 7 | 8 | def date(*args): 9 | """Create a moment.""" 10 | return Moment(*args) 11 | 12 | 13 | def now(): 14 | """Create a date from the present time.""" 15 | return Moment.now() 16 | 17 | 18 | def utc(*args): 19 | """Create a date using the UTC time zone.""" 20 | return Moment.utc(*args) 21 | 22 | 23 | def utcnow(): 24 | """UTC equivalent to `now` function.""" 25 | return Moment.utcnow() 26 | 27 | 28 | def unix(timestamp, utc=False): 29 | """Create a date from a Unix timestamp.""" 30 | return Moment.unix(timestamp, utc) 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup and installation for the package. 3 | """ 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | setup( 11 | name="moment", 12 | version="0.12.1", 13 | url="http://github.com/zachwill/moment", 14 | author="Zach Williams", 15 | author_email="hey@zachwill.com", 16 | description="Dealing with dates and times should be easy", 17 | keywords=["moment", "dates", "times", "zachwill"], 18 | packages=[ 19 | "moment" 20 | ], 21 | install_requires=[ 22 | "dateparser>=0.7", 23 | "pytz>=2018.9", 24 | "times>=0.7", 25 | ], 26 | license="MIT", 27 | classifiers=[ 28 | "Development Status :: 1 - Planning", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: MIT License", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 2.7", 33 | "Programming Language :: Python :: 3.6", 34 | "Programming Language :: Python :: 3.7", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /moment/parse.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import dateparser 4 | 5 | from .utils import STRING_TYPES 6 | 7 | 8 | def parse_date_and_formula(*args): 9 | """Doesn't need to be part of core Moment class.""" 10 | date, formula = _parse_arguments(*args) 11 | parse_settings = {"PREFER_DAY_OF_MONTH": "first"} 12 | if date and formula: 13 | if isinstance(date, datetime): 14 | return date, formula 15 | if '%' not in formula: 16 | formula = parse_js_date(formula) 17 | date = dateparser.parse(date, date_formats=[formula], settings=parse_settings) 18 | elif isinstance(date, list) or isinstance(date, tuple): 19 | if len(date) == 1: 20 | # Python datetime needs the month and day, too. 21 | date = [date[0], 1, 1] 22 | date = datetime(*date) 23 | elif isinstance(date, STRING_TYPES): 24 | date = dateparser.parse(date, settings=parse_settings) 25 | formula= "%Y-%m-%dT%H:%M:%S" 26 | return date, formula 27 | 28 | 29 | def _parse_arguments(*args): 30 | """Because I'm not particularly Pythonic.""" 31 | formula = None 32 | if len(args) == 1: 33 | date = args[0] 34 | elif len(args) == 2: 35 | date, formula = args 36 | else: 37 | date = args 38 | return date, formula 39 | 40 | 41 | def parse_js_date(date): 42 | """ 43 | Translate the easy-to-use JavaScript format strings to Python's cumbersome 44 | strftime format. Also, this is some ugly code -- and it's completely 45 | order-dependent. 46 | """ 47 | # AM/PM 48 | if 'A' in date: 49 | date = date.replace('A', '%p') 50 | elif 'a' in date: 51 | date = date.replace('a', '%P') 52 | # 24 hours 53 | if 'HH' in date: 54 | date = date.replace('HH', '%H') 55 | elif 'H' in date: 56 | date = date.replace('H', '%k') 57 | # 12 hours 58 | elif 'hh' in date: 59 | date = date.replace('hh', '%I') 60 | elif 'h' in date: 61 | date = date.replace('h', '%l') 62 | # Minutes 63 | if 'mm' in date: 64 | date = date.replace('mm', '%min') 65 | elif 'm' in date: 66 | date = date.replace('m', '%min') 67 | # Seconds 68 | if 'ss' in date: 69 | date = date.replace('ss', '%S') 70 | elif 's' in date: 71 | date = date.replace('s', '%S') 72 | # Milliseconds 73 | if 'SSS' in date: 74 | date = date.replace('SSS', '%3') 75 | # Years 76 | if 'YYYY' in date: 77 | date = date.replace('YYYY', '%Y') 78 | elif 'YY' in date: 79 | date = date.replace('YY', '%y') 80 | # Months 81 | if 'MMMM' in date: 82 | date = date.replace('MMMM', '%B') 83 | elif 'MMM' in date: 84 | date = date.replace('MMM', '%b') 85 | elif 'MM' in date: 86 | date = date.replace('MM', '%m') 87 | elif 'M' in date: 88 | date = date.replace('M', '%m') 89 | # Days of the week 90 | if 'dddd' in date: 91 | date = date.replace('dddd', '%A') 92 | elif 'ddd' in date: 93 | date = date.replace('ddd', '%a') 94 | elif 'dd' in date: 95 | date = date.replace('dd', '%w') 96 | elif 'd' in date: 97 | date = date.replace('d', '%u') 98 | # Days of the year 99 | if 'DDDD' in date: 100 | date = date.replace('DDDD', '%j') 101 | elif 'DDD' in date: 102 | date = date.replace('DDD', '%j') 103 | # Days of the month 104 | elif 'DD' in date: 105 | date = date.replace('DD', '%d') 106 | elif 'D' in date: 107 | date = date.replace('D', '%d') 108 | # Moment.js shorthand 109 | elif 'L' in date: 110 | date = date.replace('L', '%Y-%m-%dT%H:%M:%S') 111 | # A necessary evil right now... 112 | if '%min' in date: 113 | date = date.replace('%min', '%M') 114 | return date 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | moment 2 | ====== 3 | 4 | A Python library for dealing with dates/times. Inspired by [**Moment.js**][moment] and 5 | Kenneth Reitz's [**Requests**][requests] library. Ideas were also taken from the 6 | [**Times**][times] Python module. 7 | 8 | [moment]: http://momentjs.com/docs/ 9 | [requests]: http://docs.python-requests.org/ 10 | [times]: https://github.com/nvie/times 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | I would advise that this is beta-quality software. You might be interested in: 17 | 18 | - the [`arrow` package][arrow] 19 | - the [`pendulum` package][pendulum] 20 | 21 | [arrow]: https://github.com/crsmithdev/arrow/ 22 | [pendulum]: https://github.com/sdispater/pendulum 23 | 24 | Still want to use `moment`? 25 | 26 | `pip install moment` 27 | 28 | 29 | Usage 30 | ----- 31 | 32 | ```python 33 | import moment 34 | from datetime import datetime 35 | 36 | # Create a moment from a string 37 | moment.date("12-18-2012") 38 | 39 | # Create a moment with a specified strftime format 40 | moment.date("12-18-2012", "%m-%d-%Y") 41 | 42 | # Moment uses the awesome dateparser library behind the scenes 43 | moment.date("2012-12-18") 44 | 45 | # Create a moment with words in it 46 | moment.date("December 18, 2012") 47 | 48 | # Create a moment that would normally be pretty hard to do 49 | moment.date("2 weeks ago") 50 | 51 | # Create a moment from the current datetime 52 | moment.now() 53 | 54 | # The moment can also be UTC-based 55 | moment.utcnow() 56 | 57 | # Create a moment with the UTC time zone 58 | moment.utc("2012-12-18") 59 | 60 | # Create a moment from a Unix timestamp 61 | moment.unix(1355875153626) 62 | 63 | # Create a moment from a Unix UTC timestamp 64 | moment.unix(1355875153626, utc=True) 65 | 66 | # Return a datetime instance 67 | moment.date(2012, 12, 18).date 68 | 69 | # We can do the same thing with the UTC method 70 | moment.utc(2012, 12, 18).date 71 | 72 | # Create and format a moment using Moment.js semantics 73 | moment.now().format("YYYY-M-D") 74 | 75 | # Create and format a moment with strftime semantics 76 | moment.date(2012, 12, 18).strftime("%Y-%m-%d") 77 | 78 | # Use the special `%^` combo to add a date suffix (1st, 2nd, 3rd, 4th, etc) 79 | moment.date(2012, 12, 18).strftime("%B %-d%^, %Y") 80 | 81 | # Update your moment's time zone 82 | moment.date(datetime(2012, 12, 18)).locale("US/Central").date 83 | 84 | # Alter the moment's UTC time zone to a different time zone 85 | moment.utcnow().timezone("US/Eastern").date 86 | 87 | # Set and update your moment's time zone. For instance, I'm on the 88 | # west coast, but want NYC's current time. 89 | moment.now().locale("US/Pacific").timezone("US/Eastern") 90 | 91 | # In order to manipulate time zones, a locale must always be set or 92 | # you must be using UTC. 93 | moment.utcnow().timezone("US/Eastern").date 94 | 95 | # You can also clone a moment, so the original stays unaltered 96 | now = moment.utcnow().timezone("US/Pacific") 97 | future = now.clone().add(weeks=2) 98 | ``` 99 | 100 | Chaining 101 | -------- 102 | 103 | Moment allows you to chain commands, which turns out to be super useful. 104 | 105 | ```python 106 | # Customize your moment by chaining commands 107 | moment.date(2012, 12, 18).add(days=2).subtract(weeks=3).date 108 | 109 | # Imagine trying to do this with datetime, right? 110 | moment.utcnow().add(years=3, months=2).format("YYYY-M-D h:m A") 111 | 112 | # You can use multiple keyword arguments 113 | moment.date(2012, 12, 19).add(hours=1, minutes=2, seconds=3) 114 | 115 | # And, a similar subtract example... 116 | moment.date(2012, 12, 19, 1, 2, 3).subtract(hours=1, minutes=2, seconds=3) 117 | 118 | # In addition to adding/subtracting, we can also replace values 119 | moment.now().replace(hours=5, minutes=15, seconds=0).epoch() 120 | 121 | # And, if you'd prefer to keep the microseconds on your epoch value 122 | moment.now().replace(hours=5, minutes=15, seconds=0).epoch(rounding=False) 123 | 124 | # Years, months, and days can also be set 125 | moment.now().replace(years=1984, months=1, days=1, hours=0, minutes=0, seconds=0) 126 | 127 | # Also, datetime properties are available 128 | moment.utc(2012, 12, 19).year == 2012 129 | 130 | # Including plural ones (since I'm bad at remembering) 131 | moment.now().seconds 132 | 133 | # We can also manipulate to preferred weekdays, such as Monday 134 | moment.date(2012, 12, 19).replace(weekday=1).strftime("%Y-%m-%d") 135 | 136 | # Or, this upcoming Sunday 137 | moment.date("2012-12-19").replace(weekday=7).date 138 | 139 | # We can even go back to two Sundays ago 140 | moment.date(2012, 12, 19).replace(weekday=-7).format("YYYY-MM-DD") 141 | 142 | # It's also available as a property 143 | moment.utcnow().weekday 144 | 145 | # And, there's an easy way to zero out the hours, minutes, and seconds 146 | moment.utcnow().zero 147 | ``` 148 | -------------------------------------------------------------------------------- /moment/core.py: -------------------------------------------------------------------------------- 1 | from calendar import timegm 2 | from datetime import datetime, timedelta 3 | from time import timezone 4 | 5 | import pytz 6 | import times 7 | 8 | from .date import MutableDate 9 | from .parse import parse_date_and_formula, parse_js_date 10 | 11 | 12 | class Moment(MutableDate): 13 | """A class to abstract date difficulties.""" 14 | 15 | def __init__(self, *args): 16 | date, formula = parse_date_and_formula(*args) 17 | self._date = date 18 | self._formula = formula 19 | 20 | @classmethod 21 | def now(cls): 22 | """Create a moment with the current datetime.""" 23 | date = datetime.now() 24 | formula = "%Y-%m-%dT%H:%M:%S" 25 | return cls(date, formula) 26 | 27 | @classmethod 28 | def utc(cls, *args): 29 | """Create a moment from a UTC date.""" 30 | date, formula = parse_date_and_formula(*args) 31 | date = pytz.timezone("UTC").localize(date) 32 | return cls(date, formula) 33 | 34 | @classmethod 35 | def utcnow(cls): 36 | """UTC equivalent to now.""" 37 | date = pytz.timezone("UTC").localize(datetime.utcnow()) 38 | formula = "%Y-%m-%dT%H:%M:%S" 39 | return cls(date, formula) 40 | 41 | @classmethod 42 | def unix(cls, timestamp, utc=False): 43 | """Create a date from a Unix timestamp.""" 44 | # Which function are we using? 45 | if utc: 46 | func = datetime.utcfromtimestamp 47 | else: 48 | func = datetime.fromtimestamp 49 | try: 50 | # Seconds since epoch 51 | date = func(timestamp) 52 | except ValueError: 53 | # Milliseconds since epoch 54 | date = func(timestamp / 1000) 55 | # Feel like it's crazy this isn't default, but whatever. 56 | if utc: 57 | date = date.replace(tzinfo=pytz.utc) 58 | formula = "%Y-%m-%dT%H:%M:%S" 59 | return cls(date, formula) 60 | 61 | def locale(self, zone=None): 62 | """Explicitly set the time zone you want to work with.""" 63 | if not zone: 64 | self._date = datetime.fromtimestamp(timegm(self._date.timetuple())) 65 | else: 66 | try: 67 | self._date = pytz.timezone(zone).normalize(self._date) 68 | except ValueError: 69 | self._date = self._date.replace(tzinfo=pytz.timezone(zone)) 70 | return self 71 | 72 | def timezone(self, zone): 73 | """ 74 | Change the time zone and affect the current moment's time. Note, a 75 | locality must already be set. 76 | """ 77 | date = self._date 78 | try: 79 | date = times.to_local(times.to_universal(date), zone) 80 | except: 81 | date = times.to_local(date, zone) 82 | finally: 83 | self._date = date 84 | return self 85 | 86 | def _check_for_suffix(self, formula): 87 | """Check to see if a suffix is need for this date.""" 88 | if "%^" in formula: 89 | day = self.day 90 | # https://stackoverflow.com/questions/5891555/display-the-date-like-may-5th-using-pythons-strftime 91 | if 4 <= day <= 20 or 24 <= day <= 30: 92 | suffix = "th" 93 | else: 94 | suffix = ["st", "nd", "rd"][day % 10 - 1] 95 | formula = formula.replace("%^", suffix) 96 | return formula 97 | 98 | def format(self, formula): 99 | """Display the moment in a given format.""" 100 | formula = parse_js_date(formula) 101 | formula = self._check_for_suffix(formula) 102 | return self._date.strftime(formula) 103 | 104 | def strftime(self, formula): 105 | """Takes a Pythonic format, rather than the JS version.""" 106 | formula = self._check_for_suffix(formula) 107 | return self._date.strftime(formula) 108 | 109 | def diff(self, moment, measurement=None): 110 | """Return the difference between moments.""" 111 | return self - moment 112 | 113 | def done(self): 114 | """Return the datetime representation.""" 115 | return self._date 116 | 117 | def clone(self): 118 | """Return a clone of the current moment.""" 119 | clone = Moment(self._date) 120 | clone._formula = self._formula 121 | return clone 122 | 123 | def copy(self): 124 | """Same as clone.""" 125 | return self.clone() 126 | 127 | def __repr__(self): 128 | if self._date is not None: 129 | formatted = self._date.strftime("%Y-%m-%dT%H:%M:%S") 130 | return "" % (formatted) 131 | return "" 132 | 133 | def __str__(self): 134 | formatted = self._date.strftime("%Y-%m-%dT%H:%M:%S") 135 | tz = str.format("{0:+06.2f}", -float(timezone) / 3600) 136 | return formatted + tz 137 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from unittest import TestCase, main 4 | from datetime import datetime 5 | import pytz 6 | import moment 7 | 8 | 9 | class SimpleAPI(TestCase): 10 | 11 | def test_date_function_takes_a_string(self): 12 | d = moment.date("December 18, 2012", "MMMM D, YYYY") 13 | self.assertEqual(d, datetime(2012, 12, 18)) 14 | 15 | def test_date_function_with_datetime(self): 16 | d = moment.date(datetime(2012, 12, 18)) 17 | self.assertEqual(d, datetime(2012, 12, 18)) 18 | 19 | def test_date_function_with_iterable(self): 20 | d = moment.date((2012, 12, 18)) 21 | self.assertEqual(d, datetime(2012, 12, 18)) 22 | 23 | def test_date_function_with_args(self): 24 | d = moment.date(2012, 12, 18) 25 | self.assertEqual(d, datetime(2012, 12, 18)) 26 | 27 | def test_date_function_with_string(self): 28 | d = moment.date("2012-12-18") 29 | self.assertEqual(d, datetime(2012, 12, 18)) 30 | 31 | def test_date_function_with_unicode(self): 32 | d = moment.date(u"2012-12-18") 33 | self.assertEqual(d, datetime(2012, 12, 18)) 34 | 35 | def test_utc_function_with_args(self): 36 | d = moment.utc(2012, 12, 18) 37 | self.assertEqual(d, datetime(2012, 12, 18, tzinfo=pytz.utc)) 38 | 39 | def test_now_function_with_current_date(self): 40 | d = moment.now().date 41 | now = datetime.now() 42 | self.assertEqual(d.year, now.year) 43 | self.assertEqual(d.month, now.month) 44 | self.assertEqual(d.day, now.day) 45 | self.assertEqual(d.hour, now.hour) 46 | self.assertEqual(d.second, now.second) 47 | 48 | def test_utcnow_function(self): 49 | d = moment.utcnow() 50 | now = datetime.utcnow() 51 | self.assertEqual(d.year, now.year) 52 | self.assertEqual(d.month, now.month) 53 | self.assertEqual(d.day, now.day) 54 | self.assertEqual(d.hour, now.hour) 55 | self.assertEqual(d.second, now.second) 56 | 57 | def test_moment_can_transfer_between_datetime_and_moment(self): 58 | d = moment.now().date 59 | self.assertEqual(d, moment.date(d).date) 60 | 61 | def test_moment_unix_command(self): 62 | d = moment.unix(1355788800, utc=True).date 63 | expected = moment.utc((2012, 12, 18)).date 64 | self.assertEqual(d, expected) 65 | 66 | def test_moment_can_subtract_another_moment(self): 67 | d = moment.date((2012, 12, 19)) 68 | self.assertTrue(d - moment.date((2012, 12, 18))) 69 | 70 | def test_moment_can_subtract_a_datetime(self): 71 | d = moment.date((2012, 12, 19)) 72 | self.assertTrue(d - datetime(2012, 12, 18)) 73 | 74 | def test_a_datetime_can_subtract_a_moment(self): 75 | d = moment.date((2012, 12, 18)) 76 | self.assertTrue(datetime(2012, 12, 19) - d) 77 | 78 | def test_date_property(self): 79 | d = moment.date(2012, 12, 18).date 80 | self.assertEqual(d, datetime(2012, 12, 18)) 81 | 82 | def test_zero_property(self): 83 | d = moment.date(2012, 12, 18, 1, 2, 3) 84 | self.assertEqual(d.zero.date, datetime(2012, 12, 18)) 85 | 86 | def test_cloning_a_UTC_date(self): 87 | utc = moment.utc("2016-01-13T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") 88 | self.assertEqual(utc.hours, 0) 89 | self.assertEqual(utc.format("YYYY-MM-DD"), "2016-01-13") 90 | usa = utc.clone().locale("US/Eastern") 91 | self.assertEqual(usa.hours, 19) 92 | self.assertEqual(usa.format("YYYY-MM-DD"), "2016-01-12") 93 | 94 | def test_copy_method_is_same_as_clone(self): 95 | d = moment.date(2016, 5, 21) 96 | copy = d.copy().subtract(weeks=1) 97 | self.assertEqual(d, datetime(2016, 5, 21)) 98 | self.assertEqual(copy, datetime(2016, 5, 14)) 99 | 100 | 101 | class AdvancedDateParsing(TestCase): 102 | 103 | def test_today(self): 104 | d = moment.date("today").zero 105 | now = moment.now().zero 106 | self.assertEqual(d.date, now.date) 107 | 108 | def test_yesterday(self): 109 | d = moment.date("yesterday").zero 110 | expecting = moment.now().zero.subtract(days=1) 111 | self.assertEqual(d.date, expecting.date) 112 | 113 | def test_future(self): 114 | d = moment.date("tomorrow").zero 115 | expecting = moment.now().zero.add(days=1) 116 | self.assertEqual(d.date, expecting.date) 117 | 118 | def test_2_weeks_ago(self): 119 | d = moment.date("2 weeks ago").zero 120 | expecting = moment.now().zero.subtract(weeks=2) 121 | self.assertEqual(d.date, expecting.date) 122 | 123 | def test_date_with_month_as_word(self): 124 | d = moment.date("December 12, 2012").zero 125 | expecting = moment.date((2012, 12, 12)) 126 | self.assertEqual(d, expecting) 127 | 128 | def test_date_with_month_abbreviation(self): 129 | d = moment.date("Dec 12, 2012").zero 130 | expecting = moment.date((2012, 12, 12)) 131 | self.assertEqual(d, expecting) 132 | 133 | def test_date_without_days_defaults_to_first_day(self): 134 | d = moment.date("Dec 2012").zero 135 | expecting = moment.date((2012, 12, 1)) 136 | self.assertEqual(d.date, expecting.date) 137 | 138 | 139 | class Replacement(TestCase): 140 | 141 | def test_simple_chaining_commands(self): 142 | d = moment.date([2012, 12, 18]) 143 | expecting = moment.date((2012, 12, 18, 1, 2, 3)) 144 | d.replace(hours=1, minutes=2, seconds=3) 145 | self.assertEqual(d, expecting) 146 | 147 | def test_chaining_with_format(self): 148 | d = moment.utc((2012, 12, 18)) 149 | d.replace(hours=1).add(minutes=2).replace(seconds=3) 150 | expecting = "2012-12-18 01:02:03" 151 | self.assertEqual(d.format('YYYY-MM-DD hh:mm:ss'), expecting) 152 | 153 | def test_suffix_formula(self): 154 | d = moment.utc((2012, 12, 18)).zero 155 | expecting = "December 18th, 2012" 156 | self.assertEqual(d.strftime("%B %-d%^, %Y"), expecting) 157 | 158 | def test_properties_after_chaining(self): 159 | d = moment.now().replace(years=1984, months=1, days=1) 160 | self.assertEqual(d.year, 1984) 161 | 162 | def test_add_with_keywords(self): 163 | d = moment.date((2012, 12, 19)) 164 | d.add(hours=1, minutes=2, seconds=3) 165 | expecting = moment.date((2012, 12, 19, 1, 2, 3)) 166 | self.assertEqual(d, expecting) 167 | 168 | def test_subtract_with_keywords(self): 169 | d = moment.date((2012, 12, 19, 1, 2, 3)) 170 | d.subtract(hours=1, minutes=2, seconds=3) 171 | expecting = moment.date((2012, 12, 19)) 172 | self.assertEqual(d, expecting) 173 | 174 | def test_subtract_a_month(self): 175 | d = moment.date("2020-01-01") 176 | d.subtract(months=1) 177 | expecting = moment.date("2019-12-01") 178 | self.assertEqual(d, expecting) 179 | 180 | def test_subtract_several_months(self): 181 | d = moment.date("2020-11-01") 182 | d.subtract(months=20) 183 | expecting = moment.date("2019-03-01") 184 | self.assertEqual(d, expecting) 185 | 186 | def test_chaining_with_replace_method(self): 187 | d = moment.date((2012, 12, 19)) 188 | d.replace(hours=1, minutes=2, seconds=3) 189 | expecting = moment.date((2012, 12, 19, 1, 2, 3)) 190 | self.assertEqual(d, expecting) 191 | 192 | 193 | class Weekdays(TestCase): 194 | 195 | def test_weekdays_can_be_manipulated(self): 196 | d = moment.date([2012, 12, 19]) 197 | yesterday = moment.date([2012, 12, 18]) 198 | self.assertEqual(d.date.isoweekday(), 3) 199 | self.assertEqual(d.replace(weekday=3), d) 200 | self.assertEqual(d.replace(weekday=2).done(), yesterday.done()) 201 | 202 | def test_week_addition_equals_weekday_manipulation(self): 203 | d = moment.date([2012, 12, 19]) 204 | upcoming = d.clone().add('weeks', 1) 205 | expecting = moment.date([2012, 12, 26]).date 206 | self.assertEqual(upcoming, expecting) 207 | self.assertEqual(d.replace(weekday=10), upcoming) 208 | 209 | def test_weekdays_with_zeros(self): 210 | d = moment.date([2012, 12, 19]) 211 | sunday = moment.date([2012, 12, 16]) 212 | self.assertEqual(d.replace(weekday=0), sunday) 213 | 214 | def test_weekdays_with_negative_numbers(self): 215 | d = moment.date((2012, 12, 19)) 216 | expecting = moment.date([2012, 12, 9]).date 217 | self.assertEqual(d.replace(weekday=-7), expecting) 218 | 219 | def test_weekdays_with_larger_number_into_new_year(self): 220 | d = moment.date((2012, 12, 19)) 221 | expecting = moment.date("2013-01-09").date 222 | self.assertEqual(d.replace(weekday=24).date, expecting) 223 | 224 | 225 | if __name__ == '__main__': 226 | main() 227 | -------------------------------------------------------------------------------- /moment/date.py: -------------------------------------------------------------------------------- 1 | """ 2 | Where the magic happens. 3 | """ 4 | 5 | import calendar 6 | from datetime import datetime, timedelta 7 | import pytz 8 | 9 | from .utils import _iteritems 10 | 11 | 12 | # ---------------------------------------------------------------------------- 13 | # Utilities 14 | # ---------------------------------------------------------------------------- 15 | 16 | def add_month(date, number): 17 | """Add a number of months to a date.""" 18 | return month_delta(date, number) 19 | 20 | 21 | def subtract_month(date, number): 22 | """Subtract a number of months from a date.""" 23 | negative_month = number * -1 24 | return month_delta(date, negative_month) 25 | 26 | 27 | def month_delta(date, delta): 28 | """ 29 | Create a new date with a modified number of months. 30 | 31 | https://stackoverflow.com/a/22443132/485216 32 | """ 33 | month = (date.month + delta) % 12 34 | year = date.year + ((date.month) + delta - 1) // 12 35 | if not month: 36 | month = 12 37 | day = min(date.day, [ 38 | 31, 39 | 29 if year % 4 == 0 and (not year % 100 == 0 or year % 400 == 0) else 28, 40 | 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 41 | ][month - 1] 42 | ) 43 | return date.replace(day=day, month=month, year=year) 44 | 45 | 46 | # ---------------------------------------------------------------------------- 47 | # Main functionality 48 | # ---------------------------------------------------------------------------- 49 | 50 | class MutableDate(object): 51 | """Incapsulate mutable dates in one class.""" 52 | 53 | def __init__(self, date): 54 | self._date = date 55 | 56 | def add(self, key=None, amount=None, **kwds): 57 | """Add time to the original moment.""" 58 | if not key and not amount and len(kwds): 59 | for k, v in _iteritems(kwds): 60 | self.add(k, v) 61 | if key == 'years' or key == 'year': 62 | self._date = add_month(self._date, amount * 12) 63 | elif key == 'months' or key == 'month': 64 | self._date = add_month(self._date, amount) 65 | elif key == 'weeks' or key == 'week': 66 | self._date += timedelta(weeks=amount) 67 | elif key == 'days' or key == 'day': 68 | self._date += timedelta(days=amount) 69 | elif key == 'hours' or key == 'hour': 70 | self._date += timedelta(hours=amount) 71 | elif key == 'minutes' or key == 'minute': 72 | self._date += timedelta(minutes=amount) 73 | elif key == 'seconds' or key == 'second': 74 | self._date += timedelta(seconds=amount) 75 | elif key == 'milliseconds' or key == 'millisecond': 76 | self._date += timedelta(milliseconds=amount) 77 | elif key == 'microseconds' or key == 'microsecond': 78 | self._date += timedelta(microseconds=amount) 79 | return self 80 | 81 | def sub(self, key=None, amount=None, **kwds): 82 | """Just in case.""" 83 | return self.subtract(key, amount, **kwds) 84 | 85 | def subtract(self, key=None, amount=None, **kwds): 86 | """Subtract time from the original moment.""" 87 | if not key and not amount and len(kwds): 88 | for k, v in _iteritems(kwds): 89 | self.subtract(k, v) 90 | if key == 'years' or key == 'year': 91 | self._date = subtract_month(self._date, amount * 12) 92 | elif key == 'months' or key == 'month': 93 | self._date = subtract_month(self._date, amount) 94 | elif key == 'weeks' or key == 'week': 95 | self._date -= timedelta(weeks=amount) 96 | elif key == 'days' or key == 'day': 97 | self._date -= timedelta(days=amount) 98 | elif key == 'hours' or key == 'hour': 99 | self._date -= timedelta(hours=amount) 100 | elif key == 'minutes' or key == 'minute': 101 | self._date -= timedelta(minutes=amount) 102 | elif key == 'seconds' or key == 'second': 103 | self._date -= timedelta(seconds=amount) 104 | elif key == 'milliseconds' or key == 'millisecond': 105 | self._date -= timedelta(milliseconds=amount) 106 | elif key == 'microseconds' or key == 'microsecond': 107 | self._date -= timedelta(microseconds=amount) 108 | return self 109 | 110 | def replace(self, **kwds): 111 | """A Pythonic way to replace various date attributes.""" 112 | for key, value in _iteritems(kwds): 113 | if key == 'years' or key == 'year': 114 | self._date = self._date.replace(year=value) 115 | elif key == 'months' or key == 'month': 116 | self._date = self._date.replace(month=value) 117 | elif key == 'days' or key == 'day': 118 | self._date = self._date.replace(day=value) 119 | elif key == 'hours' or key == 'hour': 120 | self._date = self._date.replace(hour=value) 121 | elif key == 'minutes' or key == 'minute': 122 | self._date = self._date.replace(minute=value) 123 | elif key == 'seconds' or key == 'second': 124 | self._date = self._date.replace(second=value) 125 | elif key == 'microseconds' or key == 'microsecond': 126 | self._date = self._date.replace(microsecond=value) 127 | elif key == 'weekday': 128 | self._weekday(value) 129 | return self 130 | 131 | def epoch(self, rounding=True, milliseconds=False): 132 | """Milliseconds since epoch.""" 133 | zero = datetime.utcfromtimestamp(0) 134 | try: 135 | delta = self._date - zero 136 | except TypeError: 137 | zero = zero.replace(tzinfo=pytz.utc) 138 | delta = self._date - zero 139 | seconds = delta.total_seconds() 140 | if rounding: 141 | seconds = round(seconds) 142 | if milliseconds: 143 | seconds *= 1000 144 | return int(seconds) 145 | 146 | def _weekday(self, number): 147 | """Mutate the original moment by changing the day of the week.""" 148 | weekday = self._date.isoweekday() 149 | if number < 0: 150 | days = abs(weekday - number) 151 | else: 152 | days = weekday - number 153 | delta = self._date - timedelta(days) 154 | self._date = delta 155 | return self 156 | 157 | def isoformat(self): 158 | """Return the date's ISO 8601 string.""" 159 | return self._date.isoformat() 160 | 161 | @property 162 | def zero(self): 163 | """Get rid of hour, minute, second, and microsecond information.""" 164 | self.replace(hours=0, minutes=0, seconds=0, microseconds=0) 165 | return self 166 | 167 | @property 168 | def datetime(self): 169 | """Return the mutable date's inner datetime format.""" 170 | return self._date 171 | 172 | @property 173 | def date(self): 174 | """Access the internal datetime variable.""" 175 | return self._date 176 | 177 | @property 178 | def year(self): 179 | return self._date.year 180 | 181 | @property 182 | def month(self): 183 | return self._date.month 184 | 185 | @property 186 | def day(self): 187 | return self._date.day 188 | 189 | @property 190 | def weekday(self): 191 | return self._date.isoweekday() 192 | 193 | @property 194 | def hour(self): 195 | return self._date.hour 196 | 197 | @property 198 | def hours(self): 199 | return self._date.hour 200 | 201 | @property 202 | def minute(self): 203 | return self._date.minute 204 | 205 | @property 206 | def minutes(self): 207 | return self._date.minute 208 | 209 | @property 210 | def second(self): 211 | return self._date.second 212 | 213 | @property 214 | def seconds(self): 215 | return self._date.second 216 | 217 | @property 218 | def microsecond(self): 219 | return self._date.microsecond 220 | 221 | @property 222 | def microseconds(self): 223 | return self._date.microsecond 224 | 225 | @property 226 | def tzinfo(self): 227 | return self._date.tzinfo 228 | 229 | def __sub__(self, other): 230 | if isinstance(other, datetime): 231 | return self._date - other 232 | elif isinstance(other, type(self)): 233 | return self._date - other.date 234 | 235 | def __rsub__(self, other): 236 | return self.__sub__(other) 237 | 238 | def __lt__(self, other): 239 | if isinstance(other, datetime): 240 | return self._date < other 241 | elif isinstance(other, type(self)): 242 | return self._date < other.date 243 | 244 | def __le__(self, other): 245 | if isinstance(other, datetime): 246 | return self._date <= other 247 | elif isinstance(other, type(self)): 248 | return self._date <= other.date 249 | 250 | def __eq__(self, other): 251 | if isinstance(other, datetime): 252 | return self._date == other 253 | elif isinstance(other, type(self)): 254 | return self._date == other.date 255 | 256 | def __ne__(self, other): 257 | if isinstance(other, datetime): 258 | return self._date != other 259 | elif isinstance(other, type(self)): 260 | return self._date != other.date 261 | 262 | def __gt__(self, other): 263 | if isinstance(other, datetime): 264 | return self._date > other 265 | elif isinstance(other, type(self)): 266 | return self._date > other.date 267 | 268 | def __ge__(self, other): 269 | if isinstance(other, datetime): 270 | return self._date >= other 271 | elif isinstance(other, type(self)): 272 | return self._date >= other.date 273 | --------------------------------------------------------------------------------