"""
This module contains the class to manage timestamps
"""
# For type annotation
from __future__ import annotations
# from typing import Union
import copy
import random
[docs]class ObsTime:
"""
Class to represent the phenomenom time of an observation.
"""
BASE_YEAR = 2000
UNIX_BASE_YEAR = 1970
ROUND_TO_SEC = 1
__READ_FMT = "2D/2M/4Y 2h:2m:2s"
__PRINT_FMT = "2D/2M/4Y 2h:2m:2s"
__PRECOMPILED_READ_FMT = [
("2D", 0),
("2M", 3),
("4Y", 6),
("2h", 11),
("2m", 14),
("2s", 17),
]
__fmt_nd = ["{:01d}", "{:02d}", "{:03d}", "{:04d}"]
__codes = [
"1D", "2D",
"1M", "2M",
"2Y", "4Y",
"1h", "2h",
"1m", "2m",
"1s", "2s",
"1z", "2z", "3z",
]
__day_per_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
__month_names = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
__day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
__day_of_base_date = 3 # Thu 01/01/1970
[docs] def __init__(self, year: int = 1970, month: int = 1, day: int = 1, hour: int = 0, min: int = 0, sec: int = 0, ms: int = 0, zone: int = 0):
"""__init__ Constructor of :class:`ObsTime` class
Default value : 01/01/1970 00:00:00.000
:param year: Year, defaults to 1970
:param month: month defaults to 1
:param day: day of month, defaults to 1
:param hour: your, defaults to 0
:param min: minute, defaults to 0
:param sec: second, defaults to 0
:param ms: millisecond, defaults to 0
:param zone: time zone, defaults to 0
"""
if isinstance(year, str):
time = ObsTime.readTimestamp(year)
self.day = time.day
self.month = time.month
self.year = time.year
self.hour = time.hour
self.min = time.min
self.sec = time.sec
self.ms = time.ms
self.zone = zone
else:
self.day = day
self.month = month
self.year = year
self.hour = hour
self.min = min
self.sec = sec
self.ms = ms
self.zone = zone
[docs] def copy(self) -> ObsTime:
"""Copy the object
:return: copy of current object
"""
return copy.deepcopy(self)
[docs] @staticmethod
def readUnixTime(elapsed_seconds: float) -> ObsTime:
"""Converting elapsed float seconds since 01/01/1970 in ObsTime
**Warning:** does not consider leap seconds (31 since 1970)
:param elapsed_seconds: Elapsed seconds
:return: Coverted time
"""
'''
# elapsed is in second or in millisecond ?
from datetime import datetime
from calendar import timegm
nowobj = datetime.now()
t = datetime(nowobj.year, nowobj.month, nowobj.day, 0, 0, 0)
tps = timegm(t.timetuple())
if elapsed_seconds > tps:
elapsed_seconds = elapsed_seconds / 1000
'''
time = ObsTime()
SECOND_PER_YEAR = 86400 * 365
# SECOND_PER_MONTH = 86400 * 28
# Year
sec = 0
year = ObsTime.UNIX_BASE_YEAR
while sec < elapsed_seconds - SECOND_PER_YEAR:
sec += SECOND_PER_YEAR
if year % 4 == 0:
sec += 86400
year += 1
time.year = year
elapsed_seconds -= sec
# Month
month = 0
for i in range(12):
sec_on_month = ObsTime.__day_per_month[month] * 86400
if (month == 1) and (year % 4 == 0):
sec_on_month += 86400
if elapsed_seconds < sec_on_month:
break
elapsed_seconds -= sec_on_month
month += 1
time.month = month + 1
# Day
time.day = (int)(elapsed_seconds / 86400) + 1
elapsed_seconds -= (time.day - 1) * 86400
# Hour
time.hour = (int)(elapsed_seconds / 3600)
elapsed_seconds -= time.hour * 3600
# Minute
time.min = (int)(elapsed_seconds / 60)
elapsed_seconds -= time.min * 60
# Second
time.sec = (int)(elapsed_seconds)
elapsed_seconds -= time.sec
# Milisecond
time.ms = (int)(elapsed_seconds * 1000)
return time
[docs] def toAbsTime(self) -> float:
"""Converting to elapsed float seconds since 01/01/1970 (or 01/01/UNIX_BASE_YEAR)
**Warning:** does not consider leap seconds since 1972
:return: elapsed float seconds
"""
seconds = 0
for y in range(ObsTime.UNIX_BASE_YEAR, self.year):
seconds += 86400 * 365
if y % 4 == 0:
seconds += 86400
for m in range(1, self.month):
seconds += ObsTime.__day_per_month[m - 1] * 86400
if (m == 2) and (self.year % 4 == 0):
seconds += 86400
seconds += (self.day - 1) * 86400
seconds += self.hour * 3600
seconds += self.min * 60
seconds += self.sec
seconds += self.ms / 1000.0
return seconds
[docs] @staticmethod
def random() -> ObsTime:
"""Generate random date between 01/01/1970 and 01/01/2050"""
t0 = ObsTime(2050, 1, 1, 0, 0, 0, 0)
s0 = t0.toAbsTime()
return ObsTime.readUnixTime(random.random() * s0)
[docs] @staticmethod
def now() -> ObsTime:
"""Get Current Date and Time"""
from datetime import datetime
nowobj = datetime.now()
tps = ObsTime(nowobj.year, nowobj.month, nowobj.day,
nowobj.hour, nowobj.minute, nowobj.second, 0)
return tps
[docs] def printZone(self) -> str:
"""Print zone code (Z if greenwhich) ISO 8601"""
if self.zone == 0:
return "Z"
else:
if self.zone > 0:
return "+" + "{:02d}".format(self.zone) + ":00"
else:
return "-" + "{:02d}".format(abs(self.zone)) + ":00"
[docs] def timeWithZone(self) -> str:
"""Print as Gpx time"""
fmt_save = ObsTime.getPrintFormat()
ObsTime.setPrintFormat("4Y-2M-2DT2h:2m:2s")
output = str(self) + self.printZone()
ObsTime.setPrintFormat(fmt_save)
return output
[docs] def convertToZone(self, zone: int) -> ObsTime:
"""Zone conversion
:param zone: Time zone
:return: Time for the set time zone
"""
shift = zone - self.zone
new_time = ObsTime.readUnixTime(self.toAbsTime() + 3600 * shift)
new_time.zone = zone
return new_time
[docs] def getDayOfWeek(self) -> str:
"""Return the current day of week"""
seconds = self.toAbsTime()
return ObsTime.__day_names[
((int)(seconds / 86400) + ObsTime.__day_of_base_date) % 7
]
def __fillMember(self, value, code):
"""Adding value in a field according to code"""
if code[1] == "D":
self.day = (int)(value)
if code[1] == "M":
self.month = (int)(value)
if code[1] == "h":
self.hour = (int)(value)
if code[1] == "m":
self.min = (int)(value)
if code[1] == "s":
self.sec = (int)(value)
if code == "2Y":
self.year = (int)(value) + ObsTime.BASE_YEAR
if code == "4Y":
self.year = (int)(value)
if code[1] == "z":
self.ms = (int)(value) * 10 ** (3 - (int)(code[0]))
@staticmethod
def __takeSecond(elem):
"""Methods to prepare reading timestamp format"""
return elem[1]
@staticmethod
def __rep(char: str, n: int) -> str:
"""Generate a string repeting a char
:param char: Caracter to repeate
:param n: Number or repetitions
:retunr: A string of `n` repetitions of `c`
"""
output = ""
for i in range(n):
output += char
return output
@staticmethod
def __precompileReadFmt(format: str) -> list[tuple[str, int]]:
"""Precompile the reader
:param format: A format string
:return: A list of precompiled symbols
"""
shift = 0
PRECOMPILED_LIST = []
# Replacing '*' symbols
index = format.find("*")
while index >= 0:
n = (int)(format[index - 1])
format = format[: index - 1] + ObsTime.__rep("x", n) + format[index + 1 :]
index = format.find("*")
# Filling preccompiled list
for i in range(len(ObsTime.__codes)):
code = ObsTime.__codes[i]
index = format.find(code)
if index < 0:
continue
PRECOMPILED_LIST.append((code, index))
PRECOMPILED_LIST.sort(key=ObsTime.__takeSecond)
# Shift to manage '4Y', '1X' and '3z' symbols
for i in range(len(PRECOMPILED_LIST)):
PRECOMPILED_LIST[i] = (
PRECOMPILED_LIST[i][0],
PRECOMPILED_LIST[i][1] + shift,
)
shift += (int)(PRECOMPILED_LIST[i][0][0]) - 2
return PRECOMPILED_LIST
[docs] @staticmethod
def readTimestamp(timeAsString: str) -> ObsTime:
"""Build timestamp from string according to READ_FMT
:param timeAsString: Timestamp in string format
"""
time = ObsTime()
PCL = ObsTime.__PRECOMPILED_READ_FMT
for i in range(len(PCL)):
index = PCL[i][1]
time.__fillMember(
timeAsString[index : index + (int)(PCL[i][0][0])], PCL[i][0]
)
return time
# ------------------------------------------------------------
# Remplacing substring of length 'length', starting at pos id
# in string 'chaine' with new string 'new'
# ------------------------------------------------------------
@staticmethod
def __replace(chaine: str, id: int, length: int, new: str) -> str:
"""Remplacing substring of length `length`, starting at pos `id`
in string `chaine` with new string `new`
:param chaine: String to remplace
:param id: Starting position of replacement
:param length: Length of substring to remplace
:param new: String of replacement
:return: Replaced string
"""
return chaine[:id] + new + chaine[id + length :]
[docs] def __str__(self) -> str:
"""Prints timestamp according to PRINT_FMT specification
:retunr: the timestamp to print
"""
subst = [
self.day,
self.day,
self.month,
self.month,
self.year % 100,
self.year,
self.hour,
self.hour,
self.min,
self.min,
self.sec,
self.sec,
(int)((self.ms + 50) / 100),
(int)((self.ms + 5) / 10),
self.ms,
]
output = ObsTime.__PRINT_FMT
for i in range(len(ObsTime.__codes)):
code = ObsTime.__codes[i]
index = output.find(code)
while index >= 0:
value = (int)(code[0])
output = ObsTime.__replace(
output, index, 2, ObsTime.__fmt_nd[value - 1].format(subst[i])
)
index = output.find(code)
index = output.find("\\")
while index >= 0:
output = ObsTime.__replace(output, index, 1, "")
index = output.find("@")
return output
[docs] def addSec(self, nb: int) -> ObsTime:
"""Add `nb` seconds to current time
:param nb: Number of seconds to add
:return: A :class:`ObsTime` incremented of `nb` second(s)
"""
sec = self.toAbsTime() + nb
return ObsTime.readUnixTime(sec)
[docs] def addMin(self, nb: int) -> ObsTime:
"""Add `nb` minutes to current time
:param nb: Number of minutes to add
:return: A :class:`ObsTime` incremented of `nb` minute(s)
"""
sec = self.toAbsTime() + nb * 60
return ObsTime.readUnixTime(sec)
[docs] def addHour(self, nb):
"""Add `nb` hours to current time
:param nb: Number of hours to add
:return: A :class:`ObsTime` incremented of `nb` hour(s)
"""
sec = self.toAbsTime() + nb * 3600
return ObsTime.readUnixTime(sec)
[docs] def addDay(self, nb):
"""Add `nb` days to current time
:param nb: Number of days to add
:return: A :class:`ObsTime` incremented of `nb` day(s)
"""
sec = self.toAbsTime() + nb * 86400
return ObsTime.readUnixTime(sec)
def __sub__(self, time: ObsTime) -> float:
"""Difference between 2 dates
:param time: :class:`ObsTime` to substract
:return: Difference (in floating point seconds) between 2 date
"""
return self.toAbsTime() - time.toAbsTime()
def __eq__(self, time: ObsTime) -> bool:
"""Tests if two timestamps are strictly equal (up to 1 ms)
:param time: :class:`ObsTime` to compare
"""
if self.ms != time.ms:
return False
if self.sec != time.sec:
return False
if self.min != time.min:
return False
if self.hour != time.hour:
return False
if self.day != time.day:
return False
if self.month != time.month:
return False
if self.year != time.year:
return False
return True
def __ne__(self, time: ObsTime) -> bool:
"""Test if two timestamps are different
:param time: :class:`ObsTime` to compare
"""
return not (time == self)
def __gt__(self, time: ObsTime) -> bool:
"""Tests chronological order of two timestamps
:param time: :class:`ObsTime` to compare
"""
if self.year != time.year:
return self.year > time.year
if self.month != time.month:
return self.month > time.month
if self.day != time.day:
return self.day > time.day
if self.hour != time.hour:
return self.hour > time.hour
if self.min != time.min:
return self.min > time.min
if self.sec != time.sec:
return self.sec > time.sec
return self.ms > time.ms
def __lt__(self, time: ObsTime) -> bool:
"""Tests chronological order of two timestamps
:param time: :class:`ObsTime` to compare
"""
if self.year != time.year:
return self.year < time.year
if self.month != time.month:
return self.month < time.month
if self.day != time.day:
return self.day < time.day
if self.hour != time.hour:
return self.hour < time.hour
if self.min != time.min:
return self.min < time.min
if self.sec != time.sec:
return self.sec < time.sec
return self.ms < time.ms
def __ge__(self, time: ObsTime) -> bool:
"""Inverse of :method:`__gt__`
:param time: :class:`ObsTime` to compare
"""
return not (self < time)
def __le__(self, time: ObsTime) -> bool:
"""Inverse of :method:`__lt__`
:param time: :class:`ObsTime` to compare
"""
return not (self > time)