Add date module.

pull/31/head
peterhinch 2023-02-23 17:24:56 +00:00
rodzic e15e3219b2
commit 4691354a40
3 zmienionych plików z 289 dodań i 0 usunięć

Wyświetl plik

@ -42,6 +42,7 @@ and modules which are documented and supported.
4.12 [Quaternions](./README.md#412-quaternions) Scale, move and rotate 3D objects with minimal mathematics.
4.13 [A Pyboard power meter](./README.md#413-a-pyboard-power-meter) One of my own projects.
4.14 [NTP time](./README.md#414-ntp-time) More portable than official driver with other benefits.
4.15 [Date](./README.md#415-date) Small and simple classes for handling dates.
5. [Module Index](./README.md#5-module-index) Supported code. Device drivers, GUI's, utilities.
5.1 [uasyncio](./README.md#51-uasyncio) Tutorial and drivers for asynchronous coding.
5.2 [Memory Device Drivers](./README.md#52-memory-device-drivers) Drivers for nonvolatile memory devices.
@ -341,6 +342,20 @@ and point the MicroPython device at the local server with:
ntptime.host="192.168.0.10" # Server address.
ntptime.time()
```
## 4.15 Date
The official [datetime module](https://github.com/micropython/micropython-lib/tree/master/python-stdlib/datetime)
is fully featured but substantial. This `Date` class has no concept of time,
but is very compact. Dates are stored as a small int. Contrary to normal MP
practice, properties are used. This allows basic arithmetic syntax while
ensuring automatic rollover. The speed penalty of properties is unlikely to be
a factor in date operations.
The `Date` class provides basic arithmetic and comparison methods. The
`DateCal` subclass adds pretty printing and methods to assist in creating
calendars.
The classes are documented [here](./date/DATE.md)
##### [Index](./README.md#0-index)

114
date/DATE.md 100644
Wyświetl plik

@ -0,0 +1,114 @@
# Simple Date classes
The official [datetime module](https://github.com/micropython/micropython-lib/tree/master/python-stdlib/datetime)
is fully featured but substantial. This `Date` class has no concept of time,
but is very compact. Dates are stored as a small int. Contrary to normal MP
practice, properties are used. This allows basic arithmetic syntax while
ensuring automatic rollover. The speed penalty of properties is unlikely to be
a factor in date operations.
The `Date` class provides basic arithmetic and comparison methods. The
`DateCal` subclass adds pretty printing and methods to assist in creating
calendars.
[Return to main readme](../README.md)
# Date class
The `Date` class embodies a single date value which may be modified, copied
and compared with other `Date` instances.
## Constructor
This takes a single optional arg:
* `lt=None` By default the date is initialised from system time. To set the
date from another time source, a valid
[localtime/gmtime](http://docs.micropython.org/en/latest/library/time.html#time.localtime)
tuple may be passed.
## Method
* `now` Arg `lt=None`. Sets the instance to the current date, from system time
or `lt` as described above.
## Writeable properties
* `year` e.g. 2023.
* `month` 1 == January. May be set to any number, years will roll over if
necessary. e.g. `d.month += 15` or `d.month -= 1`.
* `mday` Adjust day in current month. Allowed range `1..month_length`.
* `day` Days since epoch. Note that the epoch varies with platform - the value
may be treated as an opaque small integer. Use to adjust a date with rollover
(`d.day += 7`) or to assign one date to another (`date2.day = date1.day`). May
also be used to represnt a date as a small int for saving to a file.
## Read-only property
* `wday` Day of week. 0==Monday 6==Sunday.
## Date comparisons
Python "magic methods" enable date comparisons using standard operators `<`,
`<=`, `>`, `>=`, `==`, `!=`.
# DateCal class
This adds pretty formatting and functionality to return additional information
about the current date. The added methods and properties do not change the
date value. Primarily intended for calendars.
## Constructor
This takes a single optional arg:
* `lt=None` See `Date` constructor.
## Methods
* `time_offset` arg `hr=6`. This returns 0 or 1, being the offset in hours of
UK local time to UTC. By default the change occurs when the date changes at
00:00 UTC on the last Sunday in March and October. If an hour value is passed,
the change will occur at the correct 01:00 UTC. This method will need to be
adapted for other geographic locations.
* `wday_n` arg `mday=1`. Return the weekday for a given day of the month.
* `mday_list` arg `wday`. Given a weekday, for the current month return an
ordered list of month days matching that weekday.
## Read-only properties
* `month_length` Length of month in days.
* `day_str` Day of week as a string, e.g. "Wednesday".
* `month_str` Month as a string, e.g. "August".
## Class variables
* `days` A 7-tuple `("Monday", "Tuesday"...)`
* `months` A 12-tuple `("January", "February",...)`
# Example usage
```python
from date import Date
d = Date()
d.month = 1 # Set to January
d.month -= 2 # Date changes to same mday in November previous year.
d.mday = 25 # Set absolute day of month
d.day += 7 # Advance date by one week. Month/year rollover is handled.
today = Date()
if d == today: # Date comparisons
# do something
new_date = Date()
new_date.day = d.day # Assign d to new_date: now new_date == d.
print(d) # Basic numeric print.
```
The DateCal class:
```python
from date import DateCal
d = DateCal()
# Correct a UK clock for DST
d.now()
hour = (hour_utc + d.time_offset(hour_utc)) % 24
print(d) # Pretty print
x = d.wday_n(1) # Get day of week of 1st day of month
sundays = d.mday_list(6) # List Sundays for the month.
wday_last = d.wday_n(d.month_length) # Weekday of last day of month
```

160
date/date.py 100644
Wyświetl plik

@ -0,0 +1,160 @@
# date.py Minimal Date class for micropython
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from time import mktime, localtime
_SECS_PER_DAY = const(86400)
def leap(year):
return bool((not year % 4) ^ (not year % 100))
class Date:
def __init__(self, lt=None):
self.now(lt)
def now(self, lt=None):
self._lt = list(localtime()) if lt is None else list(lt)
self._update()
def _update(self, ltmod=True): # If ltmod is False ._cur has been changed
if ltmod: # Otherwise ._lt has been modified
self._lt[3] = 6
self._cur = mktime(self._lt) // _SECS_PER_DAY
self._lt = list(localtime(self._cur * _SECS_PER_DAY))
def _mlen(self, d=bytearray((31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31))):
days = d[self._lt[1] - 1]
return days if days else (29 if leap(self._lt[0]) else 28)
@property
def year(self):
return self._lt[0]
@year.setter
def year(self, v):
if self.mday == 29 and self.month == 2 and not leap(v):
self.mday = 28 # Ensure it doesn't skip a month
self._lt[0] = v
self._update()
@property
def month(self):
return self._lt[1]
# Can write d.month = 4 or d.month += 15
@month.setter
def month(self, v):
y, m = divmod(v - 1, 12)
self._lt[0] += y
self._lt[1] = m + 1
self._lt[2] = min(self._lt[2], self._mlen())
self._update()
@property
def mday(self):
return self._lt[2]
@mday.setter
def mday(self, v):
if not 0 < v <= self._mlen():
raise ValueError(f"mday {v} is out of range")
self._lt[2] = v
self._update()
@property
def day(self): # Days since epoch.
return self._cur
@day.setter
def day(self, v): # Usuge: d.day += 7 or date_1.day = d.day.
self._cur = v
self._update(False) # Flag _cur change
# Read-only properties
@property
def wday(self):
return self._lt[6]
# Date comparisons
def __lt__(self, other):
return self.day < other.day
def __le__(self, other):
return self.day <= other.day
def __eq__(self, other):
return self.day == other.day
def __ne__(self, other):
return self.day != other.day
def __gt__(self, other):
return self.day > other.day
def __ge__(self, other):
return self.day >= other.day
def __str__(self):
return f"{self.year}/{self.month}/{self.mday}"
class DateCal(Date):
days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
months = (
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)
def __init__(self, lt=None):
super().__init__(lt)
@property
def month_length(self):
return self._mlen()
@property
def day_str(self):
return self.days[self.wday]
@property
def month_str(self):
return self.months[self.month - 1]
def wday_n(self, mday=1):
return (self._lt[6] - self._lt[2] + mday) % 7
def mday_list(self, wday):
ml = self._mlen() # 1 + ((wday - wday1) % 7)
d0 = 1 + ((wday - (self._lt[6] - self._lt[2] + 1)) % 7)
return [d for d in range(d0, ml + 1, 7)]
# Optional: return UK DST offset in hours. Can pass hr to ensure that time change occurs
# at 1am UTC otherwise it occurs on date change (0:0 UTC)
# offs is offset by month
def time_offset(self, hr=6, offs=bytearray((0, 0, 3, 1, 1, 1, 1, 1, 1, 10, 0, 0))):
ml = self._mlen()
wdayld = self.wday_n(ml) # Weekday of last day of month
mday_sun = self.mday_list(6)[-1] # Month day of last Sunday
m = offs[self._lt[1] - 1]
if m < 3:
return m # Deduce time offset from month alone
return int(
((self._lt[2] < mday_sun) or (self._lt[2] == mday_sun and hr <= 1)) ^ (m == 3)
) # Months where offset changes
def __str__(self):
return f"{self.day_str} {self.mday} {self.month_str} {self.year}"