1
1
"""Base time conversion functions"""
2
2
3
+ import logging
4
+
3
5
from datetime import datetime as _datetime
6
+ from datetime import date as _date
4
7
from datetime import timedelta as _timedelta
5
8
from io import StringIO as _StringIO
6
9
from typing import Optional , overload , Union
11
14
from . import gn_const as _gn_const
12
15
13
16
17
+ logger = logging .getLogger (__name__ )
18
+
19
+
14
20
def gpsweekD (yr , doy , wkday_suff = False ):
15
21
"""
16
22
Convert year, day-of-year to GPS week format: WWWWD or WWWW
@@ -58,57 +64,71 @@ class GPSDate:
58
64
Usage:
59
65
today = GPSDate("today")
60
66
tomorrow = today.next
61
- print(f"today year: {today.year}, doy: {today.dy }, GPS week and weekday: {today.gpswkD }")
62
- print(f"tomorrow year: {tomorrow.year}, doy: {tomorrow.dy }, GPS week and weekday: {tomorrow.gpswkD }")
67
+ print(f"today year: {today.year}, doy: {today.day_of_year }, GPS week and weekday: {today.gps_week_and_day_of_week }")
68
+ print(f"tomorrow year: {tomorrow.year}, doy: {tomorrow.day_of_year }, GPS week and weekday: {tomorrow.gps_week_and_day_of_week }")
63
69
"""
64
70
65
- def __init__ (self , ts : _np .datetime64 ):
66
- if isinstance (ts , str ):
67
- ts = _np .datetime64 (ts )
71
+ # For compatibility, we have accessors called 'ts' and 'timestamp'.
72
+ _internal_dt64 : _np .datetime64
73
+
74
+ def __init__ (self , time : Union [_np .datetime64 , _datetime , _date , str ]):
75
+ if isinstance (time , _np .datetime64 ):
76
+ self ._internal_dt64 = time
77
+ elif isinstance (time , (_datetime , _date , str )):
78
+ # Something we may be able to convert to a datetime64
79
+ self ._internal_dt64 = _np .datetime64 (time )
80
+ else :
81
+ raise TypeError ("GPSDate() takes only Numpy datetime64, datetime, date, or str representation of a date" )
82
+
83
+ @property
84
+ def get_datetime64 (self ) -> _np .datetime64 :
85
+ return self ._internal_dt64
68
86
69
- self .ts = ts
87
+ @property # For backwards compatibility
88
+ def ts (self ) -> _np .datetime64 :
89
+ return self .get_datetime64
70
90
71
91
@property
72
- def as_datetime (self ):
73
- """Convert to Python `datetime` object."""
74
- return self .ts . astype ( _datetime )
92
+ def as_datetime (self ) -> _datetime :
93
+ """Convert to Python `datetime` object. Note that as GPSDate has no hour, neither will this datetime """
94
+ return _pd . Timestamp ( self ._internal_dt64 ). to_pydatetime ( )
75
95
76
96
@property
77
- def yr (self ):
97
+ def yr (self ) -> str :
78
98
"""Year"""
79
99
return self .as_datetime .strftime ("%Y" )
80
100
81
101
@property
82
- def dy (self ):
102
+ def dy (self ) -> str :
83
103
"""Day of year"""
84
104
return self .as_datetime .strftime ("%j" )
85
105
86
106
@property
87
- def gpswk (self ):
107
+ def gpswk (self ) -> str :
88
108
"""GPS week"""
89
109
return gpsweekD (self .yr , self .dy , wkday_suff = False )
90
110
91
111
@property
92
- def gpswkD (self ):
112
+ def gpswkD (self ) -> str :
93
113
"""GPS week with weekday suffix"""
94
114
return gpsweekD (self .yr , self .dy , wkday_suff = True )
95
115
96
116
@property
97
117
def next (self ):
98
118
"""The following day"""
99
- return GPSDate (self .ts + 1 )
119
+ return GPSDate (self ._internal_dt64 + 1 )
100
120
101
121
@property
102
122
def prev (self ):
103
123
"""The previous day"""
104
- return GPSDate (self .ts - 1 )
124
+ return GPSDate (self ._internal_dt64 - 1 )
105
125
106
- def __str__ (self ):
126
+ def __str__ (self ) -> str :
107
127
"""Same string representation as the underlying numpy datetime64 object"""
108
- return str (self .ts )
128
+ return str (self ._internal_dt64 )
109
129
110
130
111
- def dt2gpswk (dt , wkday_suff = False , both = False ):
131
+ def dt2gpswk (dt , wkday_suff = False , both = False ) -> Union [ str , tuple [ str , str ]] :
112
132
"""
113
133
Convert the given datetime object to a GPS week (option to include day suffix)
114
134
"""
@@ -120,13 +140,41 @@ def dt2gpswk(dt, wkday_suff=False, both=False):
120
140
return gpsweekD (yr , doy , wkday_suff = False ), gpsweekD (yr , doy , wkday_suff = True )
121
141
122
142
123
- def gpswkD2dt (gpswkD ):
143
+ # TODO DEPRECATED
144
+ def gpswkD2dt (gpswkD : str ) -> _datetime :
124
145
"""
125
- Convert from GPS-Week-Day (WWWWDD) format to datetime object
146
+ DEPRECATED. This is a compatibility wrapper. Use gps_week_day_to_datetime() instead.
147
+ """
148
+ return gps_week_day_to_datetime (gpswkD )
149
+
150
+
151
+ def gps_week_day_to_datetime (gps_week_and_weekday : str ) -> _datetime :
152
+ """
153
+ Convert from GPS-Week-Day (including day: WWWWD or just week: WWWW) format to datetime object.
154
+ If a day-of-week is not provided, 0 (Sunday) is assumed.
155
+
156
+ param: str gps_week_and_weekday: A date expressed in GPS weeks with (optionally) day-of-week.
157
+ returns _datetime: The date expressed as a datetime.datetime object
126
158
"""
127
- if type (gpswkD ) != str :
128
- gpswkD = str (gpswkD )
129
- dt_64 = _gn_const .GPS_ORIGIN + _np .timedelta64 (int (gpswkD [:- 1 ]), "W" ) + _np .timedelta64 (int (gpswkD [- 1 ]), "D" )
159
+ if not isinstance (gps_week_and_weekday , str ):
160
+ raise TypeError ("GPS Week-Day must be a string" )
161
+
162
+ if not len (gps_week_and_weekday ) in (4 , 5 ):
163
+ raise ValueError (
164
+ "GPS Week-Day must be either a 4 digit week (WWWW), or a 4 digit week plus 1 digit day-of-week (WWWWD)"
165
+ )
166
+
167
+ if len (gps_week_and_weekday ) == 5 : # GPS week with day-of-week (WWWWD)
168
+ # Split into 4 digit week number, and 1 digit day number.
169
+ # Parse each as a time delta and add them to the GPS origin date.
170
+ dt_64 = (
171
+ _gn_const .GPS_ORIGIN
172
+ + _np .timedelta64 (int (gps_week_and_weekday [:- 1 ]), "W" )
173
+ + _np .timedelta64 (int (gps_week_and_weekday [- 1 ]), "D" )
174
+ )
175
+ else : # 4 digit week, no trimming needed
176
+ dt_64 = _gn_const .GPS_ORIGIN + _np .timedelta64 (int (gps_week_and_weekday ), "W" )
177
+
130
178
return dt_64 .astype (_datetime )
131
179
132
180
@@ -256,10 +304,10 @@ def mjd2datetime(mjd: _np.ndarray, seconds_frac: _np.ndarray, pea_partials=False
256
304
257
305
258
306
def mjd_to_pydatetime (mjd : float ) -> _datetime :
259
- """Convert python datetime object to corresponding Modified Julian Date
307
+ """Convert Modified Julian Date to corresponding python datetime object
260
308
261
- :param datetime.datetime dt: Python datetime of interest
262
- :return float : Corresponding Modified Julian Date
309
+ :param float mjd: Modified Julian Date of interest
310
+ :return datetime.datetime : Corresponding Python datetime
263
311
"""
264
312
mjd_epoch_dt = _datetime (2000 , 1 , 1 )
265
313
return mjd_epoch_dt + _timedelta (days = mjd - 51544.00 )
@@ -270,11 +318,72 @@ def mjd2j2000(mjd: _np.ndarray, seconds_frac: _np.ndarray, pea_partials=False) -
270
318
return datetime2j2000 (datetime )
271
319
272
320
321
+ def j2000_to_igs_dt (j2000_secs : _np .ndarray ) -> _np .ndarray :
322
+ """
323
+ Converts array of j2000 times to format string representation used by many IGS formats including Rinex and SP3.
324
+ E.g. 674913600 -> '2021-05-22T00:00:00' -> '2021 5 22 0 0 0.00000000'
325
+ :param _np.ndarray j2000_secs: Numpy NDArray of (typically epoch) times in J2000 seconds.
326
+ :return _np.ndarray: Numpy NDArray with those same times as strings.
327
+ """
328
+ datetime = j20002datetime (j2000_secs )
329
+ year = datetime .astype ("datetime64[Y]" )
330
+ month = datetime .astype ("datetime64[M]" )
331
+ day = datetime .astype ("datetime64[D]" )
332
+ hour = datetime .astype ("datetime64[h]" )
333
+ minute = datetime .astype ("datetime64[m]" )
334
+
335
+ date_y = _pd .Series (year .astype (str )).str .rjust (4 ).values
336
+ date_m = _pd .Series (((month - year ).astype ("int64" ) + 1 ).astype (str )).str .rjust (3 ).values
337
+ date_d = _pd .Series (((day - month ).astype ("int64" ) + 1 ).astype (str )).str .rjust (3 ).values
338
+
339
+ time_h = _pd .Series ((hour - day ).astype ("int64" ).astype (str )).str .rjust (3 ).values
340
+ time_m = _pd .Series ((minute - hour ).astype ("int64" ).astype (str )).str .rjust (3 ).values
341
+ # Width 12 due to one extra leading space (for easier concatenation next), then _0.00000000 format per SP3d spec:
342
+ time_s = (_pd .Series ((datetime - minute )).view ("int64" ) / 1e9 ).apply ("{:.8f}" .format ).str .rjust (12 ).values
343
+ return date_y + date_m + date_d + time_h + time_m + time_s
344
+
345
+
346
+ def j2000_to_igs_epoch_row_header_dt (j2000_secs : _np .ndarray ) -> _np .ndarray :
347
+ """
348
+ Utility wrapper function to format J2000 time values (typically epoch values) to be written as epoch header lines
349
+ within the body of SP3, Rinex, etc. files.
350
+ E.g. 674913600 -> '2021-05-22T00:00:00' -> '* 2021 5 22 0 0 0.00000000\n '
351
+ :param _np.ndarray j2000_secs: Numpy NDArray of (typically epoch) times in J2000 seconds.
352
+ :return _np.ndarray: Numpy NDArray with those same times as strings, including epoch line lead-in and newline.
353
+ """
354
+ # Add leading "* "s and trailing newlines around all values
355
+ return "* " + j2000_to_igs_dt (j2000_secs ) + "\n "
356
+
357
+
358
+ def j2000_to_sp3_head_dt (j2000secs : _np .ndarray ) -> str :
359
+ """
360
+ Utility wrapper function to format a J2000 time value for the SP3 header. Takes NDArray, but only expects one value
361
+ in it.
362
+ :param _np.ndarray j2000_secs: Numpy NDArray containing a *single* time value in J2000 seconds.
363
+ :return str: The provided time value as a string.
364
+ """
365
+ formatted_times = j2000_to_igs_dt (j2000secs )
366
+
367
+ # If making a header there should be one value. If not it's a mistake, or at best inefficient.
368
+ if len (formatted_times ) != 1 :
369
+ logger .warning (
370
+ "More than one time value passed through. This function is meant to be used to format a single value "
371
+ "in the SP3 header."
372
+ )
373
+ return formatted_times [0 ]
374
+
375
+
376
+ # TODO DEPRECATED.
273
377
def j20002rnxdt (j2000secs : _np .ndarray ) -> _np .ndarray :
274
378
"""
275
- Converts j2000 array to rinex format string representation
379
+ DEPRECATED since about version 0.0.58
380
+ TODO remove in version 0.0.59
381
+ Converts array of j2000 times to rinex format string representation
382
+ NOTE: the following is incorrect by SP3d standard; there should be another space before the seconds.
276
383
674913600 -> '2021-05-22T00:00:00' -> '* 2021 5 22 0 0 0.00000000\n '
277
384
"""
385
+ logger .warning ("j20002rnxdt() is deprecated. Please use j2000_to_igs_epoch_row_header_dt() instead." )
386
+
278
387
datetime = j20002datetime (j2000secs )
279
388
year = datetime .astype ("datetime64[Y]" )
280
389
month = datetime .astype ("datetime64[M]" )
@@ -288,6 +397,7 @@ def j20002rnxdt(j2000secs: _np.ndarray) -> _np.ndarray:
288
397
289
398
time_h = _pd .Series ((hour - day ).astype ("int64" ).astype (str )).str .rjust (3 ).values
290
399
time_m = _pd .Series ((minute - hour ).astype ("int64" ).astype (str )).str .rjust (3 ).values
400
+ # NOTE: The following may be wrong by the SP3d spec. Again, please use j2000_to_igs_epoch_row_header_dt() instead.
291
401
time_s = (_pd .Series ((datetime - minute )).view ("int64" ) / 1e9 ).apply ("{:.8f}\n " .format ).str .rjust (13 ).values
292
402
return date_y + date_m + date_d + time_h + time_m + time_s
293
403
0 commit comments