Source code for core.obs_time

# -*- coding: utf-8 -*-

"""
© Copyright Institut National de l'Information Géographique et Forestière (2020)
Contributors: 
    Yann Méneroux
Creation date: 1th november 2020

tracklib library provides a variety of tools, operators and 
functions to manipulate GPS trajectories. It is a open source contribution 
of the LASTIG laboratory at the Institut National de l'Information 
Géographique et Forestière (the French National Mapping Agency).
See: https://tracklib.readthedocs.io
 
This software is governed by the CeCILL-C license under French law and
abiding by the rules of distribution of free software. You can  use, 
modify and/ or redistribute the software under the terms of the CeCILL-C
license as circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info". 

As a counterpart to the access to the source code and rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty  and the software's author,  the holder of the
economic rights,  and the successive licensors  have only  limited
liability. 

In this respect, the user's attention is drawn to the risks associated
with loading,  using,  modifying and/or developing or reproducing the
software by the user in light of its specific status of free software,
that may mean  that it is complicated to manipulate,  and  that  also
therefore means  that it is reserved for developers  and  experienced
professionals having in-depth computer knowledge. Users are therefore
encouraged to load and test the software's suitability as regards their
requirements in conditions enabling the security of their systems and/or 
data to be ensured and,  more generally, to use and operate it in the 
same conditions as regards security. 

The fact that you are presently reading this means that you have had
knowledge of the CeCILL-C license and that you accept its terms.



This module contains the class to manage timestamps

"""
# For type annotation
from __future__ import annotations   
# from typing import Union
#from tracklib.util.exceptions import *

import copy
from datetime import datetime
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 setPrintFormat(format: str): """Set the format for printing a ObsTime object The print format is use by the :func:`ObsTime.__str__` method :param format: Format for printing ObsTime """ ObsTime.__PRINT_FMT = format
[docs] @staticmethod def setReadFormat(format: str): """Set the format for reading a ObsTime object :param format: Format for reading """ ObsTime.__READ_FMT = format ObsTime.__PRECOMPILED_READ_FMT = ObsTime.__precompileReadFmt(ObsTime.__READ_FMT)
[docs] @staticmethod def getPrintFormat() -> str: """Return the print format :return: The print format """ return ObsTime.__PRINT_FMT
[docs] @staticmethod def getReadFormat() -> str: """Return the read format :return: The read format """ return ObsTime.__READ_FMT
[docs] @staticmethod def isLeapYear(year) -> bool: return ((year % 4 == 0) and ((year % 100 != 0) or (year % 400 == 0)))
[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 ObsTime.isLeapYear(year): 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): if (month == 1) and ObsTime.isLeapYear(year): 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 ObsTime.isLeapYear(y): seconds += 86400 for m in range(1, self.month): seconds += ObsTime.__day_per_month[m - 1] * 86400 if (m == 2) and ObsTime.isLeapYear(self.year): 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""" 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": if value != "": self.ms = (int)(value) * 10 ** (3 - (int)(code[0])) else: self.ms = 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 not isinstance(time, ObsTime): return False 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)