diff --git a/gui/demos/calendar.py b/gui/demos/calendar.py index 01dba8f..872ef16 100644 --- a/gui/demos/calendar.py +++ b/gui/demos/calendar.py @@ -1,9 +1,7 @@ -# calendar.py Test Grid class. +# calendar.py Test Grid class. Requires date.py. # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2023 Peter Hinch -# As written requires a 240*320 pixel display, but could easily be reduced -# with smaller fonts. # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance @@ -16,67 +14,14 @@ from gui.core.writer import CWriter import gui.fonts.font10 as font import gui.fonts.font14 as font1 from gui.core.colors import * - -from time import mktime, localtime -SECS_PER_DAY = const(86400) - -class 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): - self.now() - - def now(self): - lt = list(localtime()) - lt[3] = 6 # Disambiguate midnight - self.cur = mktime(lt) // SECS_PER_DAY - self.update() - - def update(self, lt=None): - if lt is not None: - lt[3] = 6 - self.cur = mktime(lt) // SECS_PER_DAY - lt = localtime(self.cur * SECS_PER_DAY) - self.year = lt[0] - self.month = lt[1] - self.mday = lt[2] - self.wday = lt[6] - ml = self.mlen(self.month) - self.month_length = ml - self.wday1 = (self.wday - self.mday + 1) % 7 # Weekday of 1st of month - # Commented out code provides support for UK DST calculation - #wdayld = (self.wday1 + ml -1) % 7 # Weekday of last day of month - #self.mday_sun = ml - (wdayld + 1) % 7 # Day of month of last Sunday - - def add_days(self, n): - self.cur += n - self.update() - - def add_months(self, n): # Crude algorithm for small n - for _ in range(abs(n)): - self.cur += self.month_length if n > 0 else -self.mlen(self.month - 1) - self.update() - - def add_years(self, n): - lt = list(localtime(self.cur * SECS_PER_DAY)) - lt[0] += n - self.update(lt) - - def mlen(self, month): - days = (31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1] - year = self.year - return days if days else (28 if year % 4 else (29 if year % 100 else 28)) - +from .date import DateCal class BaseScreen(Screen): def __init__(self): super().__init__() - self.date = Date() + self.date = DateCal() wri = CWriter(ssd, font, GREEN, BLACK, False) wri1 = CWriter(ssd, font1, WHITE, BLACK, False) col = 2 @@ -87,28 +32,28 @@ class BaseScreen(Screen): self.lbl = Label(wri, row, col, text = (colwidth + 4) * cols, justify=Label.CENTRE) row = self.lbl.mrow self.grid = Grid(wri, row, col, colwidth, rows, cols, justify=Label.CENTRE) - for n, day in enumerate(Date.days): + for n, day in enumerate(DateCal.days): self.grid[[0, n]] = day[:3] row = self.grid.mrow + 4 ht = 30 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="y-", callback=self.adjust, args=(self.date.add_years, -1)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="y-", callback=self.adjust, args=("y", -1)) col = b.mcol + 2 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="y+", callback=self.adjust, args=(self.date.add_years, 1)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="y+", callback=self.adjust, args=("y", 1)) col = b.mcol + 5 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="m-", callback=self.adjust, args=(self.date.add_months, -1)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="m-", callback=self.adjust, args=("m", -1)) col = b.mcol + 2 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="m+", callback=self.adjust, args=(self.date.add_months, 1)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="m+", callback=self.adjust, args=("m", 1)) col = b.mcol + 5 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="w-", callback=self.adjust, args=(self.date.add_days, -7)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="w-", callback=self.adjust, args=("d", -7)) col = b.mcol + 2 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="w+", callback=self.adjust, args=(self.date.add_days, 7)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="w+", callback=self.adjust, args=("d", 7)) col = b.mcol + 5 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="d-", callback=self.adjust, args=(self.date.add_days, -1)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="d-", callback=self.adjust, args=("d", -1)) col = b.mcol + 2 - b = Button(wri, row, col, height=ht, shape=CIRCLE, text="d+", callback=self.adjust, args=(self.date.add_days, 1)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, text="d+", callback=self.adjust, args=("d", 1)) col = b.mcol + 5 - b = Button(wri, row, col, height=ht, shape=CIRCLE, fgcolor=BLUE, text="H", callback=self.adjust, args=(self.date.now,)) + b = Button(wri, row, col, height=ht, shape=CIRCLE, fgcolor=BLUE, text="H", callback=self.adjust, args=("h",)) #row = b.mrow + 10 col = 2 row = ssd.height - (wri1.height + 2) @@ -117,33 +62,42 @@ class BaseScreen(Screen): CloseButton(wri) # Quit the application def adjust(self, _, f, n=0): - f(n) if n else f() + d = self.date + if f == "y": + d.year += n + elif f == "m": + d.month += n + elif f == "d": + d.day += n + elif f =="h": + d.now() self.update() def update(self): def cell(): - #d.clear() if cur.year == today.year and cur.month == today.month and mday == today.mday: # Today d["fgcolor"] = RED elif mday == cur.mday: # Currency d["fgcolor"] = YELLOW + elif mday in sundays: + d["fgcolor"] = BLUE else: d["fgcolor"] = GREEN d["text"] = str(mday) self.grid[idx] = d - today = Date() + today = DateCal() cur = self.date # Currency - self.lbl.value(f"{Date.months[cur.month - 1]} {cur.year}") + self.lbl.value(f"{DateCal.months[cur.month - 1]} {cur.year}") d = {} # Args for Label.value wday = 0 + wday_1 = cur.wday_n(1) # Weekday of 1st of month mday = 1 seek = True - for idx in range(7, self.grid.ncells): - self.grid[idx] = "" + sundays = cur.mday_list(6) for idx in range(7, self.grid.ncells): if seek: # Find column for 1st of month - if wday < cur.wday1: + if wday < wday_1: self.grid[idx] = "" wday += 1 else: @@ -154,14 +108,12 @@ class BaseScreen(Screen): mday += 1 else: self.grid[idx] = "" - idx = 7 + idx = 7 # Where another row would be needed, roll over to top few cells. while mday <= cur.month_length: cell() idx += 1 mday += 1 - day = Date.days[today.wday] - month = Date.months[today.month - 1] - self.lblnow.value(f"{day} {today.mday} {month} {today.year}") + self.lblnow.value(f"{today.day_str} {today.mday} {today.month_str} {today.year}") def test(): diff --git a/gui/demos/date.py b/gui/demos/date.py new file mode 100644 index 0000000..2601ee0 --- /dev/null +++ b/gui/demos/date.py @@ -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}"