dfdatetime 20260730__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1732 @@
1
+ """Time elements implementation."""
2
+
3
+ import decimal
4
+
5
+ from dfdatetime import definitions
6
+ from dfdatetime import factory
7
+ from dfdatetime import interface
8
+ from dfdatetime import precisions
9
+
10
+
11
+ class TimeElements(interface.DateTimeValues):
12
+ """Time elements.
13
+
14
+ Time elements contain separate values for year, month, day of month,
15
+ hours, minutes and seconds.
16
+
17
+ Attributes:
18
+ is_local_time (bool): True if the date and time value is in local time.
19
+ """
20
+
21
+ # Maps the RFC 822, RFC 1123 and RFC 2822 definitions to their corresponding
22
+ # integer values.
23
+ _RFC_MONTH_MAPPINGS = {
24
+ "Jan": 1,
25
+ "Feb": 2,
26
+ "Mar": 3,
27
+ "Apr": 4,
28
+ "May": 5,
29
+ "Jun": 6,
30
+ "Jul": 7,
31
+ "Aug": 8,
32
+ "Sep": 9,
33
+ "Oct": 10,
34
+ "Nov": 11,
35
+ "Dec": 12,
36
+ }
37
+
38
+ _RFC_TIME_ZONE_MAPPINGS = {
39
+ "UT": 0,
40
+ "GMT": 0,
41
+ "EST": -5,
42
+ "EDT": -4,
43
+ "CST": -6,
44
+ "CDT": -5,
45
+ "MST": -7,
46
+ "MDT": -6,
47
+ "PST": -8,
48
+ "PDT": -7,
49
+ "A": -1,
50
+ "B": -2,
51
+ "C": -3,
52
+ "D": -4,
53
+ "E": -5,
54
+ "F": -6,
55
+ "G": -7,
56
+ "H": -8,
57
+ "I": -9,
58
+ "K": -10,
59
+ "L": -11,
60
+ "M": -12,
61
+ "N": 1,
62
+ "O": 2,
63
+ "P": 3,
64
+ "Q": 4,
65
+ "R": 5,
66
+ "S": 6,
67
+ "T": 7,
68
+ "U": 8,
69
+ "V": 9,
70
+ "W": 10,
71
+ "X": 11,
72
+ "Y": 12,
73
+ "Z": 0,
74
+ }
75
+
76
+ _RFC_WEEKDAYS = frozenset(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
77
+
78
+ def __init__(
79
+ self,
80
+ is_delta=False,
81
+ precision=None,
82
+ time_elements_tuple=None,
83
+ time_zone_offset=None,
84
+ ):
85
+ """Initializes time elements.
86
+
87
+ Args:
88
+ is_delta (Optional[bool]): True if the date and time value is relative to
89
+ another date and time value.
90
+ precision (Optional[str]): precision of the date and time value, which
91
+ should be one of the PRECISION_VALUES in definitions.
92
+ time_elements_tuple (Optional[tuple[int, int, int, int, int, int]]):
93
+ time elements, contains year, month, day of month, hours, minutes and
94
+ seconds.
95
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
96
+ from UTC or None if not set.
97
+
98
+ Raises:
99
+ ValueError: if the time elements tuple is invalid.
100
+ """
101
+ super().__init__(
102
+ is_delta=is_delta,
103
+ precision=precision or definitions.PRECISION_1_SECOND,
104
+ time_zone_offset=time_zone_offset,
105
+ )
106
+ self._number_of_seconds = None
107
+ self._time_elements_tuple = time_elements_tuple
108
+
109
+ if time_elements_tuple:
110
+ number_of_elements = len(time_elements_tuple)
111
+ if number_of_elements < 6:
112
+ raise ValueError(
113
+ (
114
+ f"Invalid time elements tuple at least 6 elements required,"
115
+ f"got: {number_of_elements:d}"
116
+ )
117
+ )
118
+
119
+ self._number_of_seconds = self._GetNumberOfSecondsFromElements(
120
+ time_elements_tuple[0],
121
+ time_elements_tuple[1],
122
+ time_elements_tuple[2],
123
+ time_elements_tuple[3],
124
+ time_elements_tuple[4],
125
+ time_elements_tuple[5],
126
+ )
127
+
128
+ def _GetNormalizedTimestamp(self):
129
+ """Retrieves the normalized timestamp.
130
+
131
+ Returns:
132
+ decimal.Decimal: normalized timestamp, which contains the number of
133
+ seconds since January 1, 1970 00:00:00 and a fraction of second used
134
+ for increased precision, or None if the normalized timestamp cannot be
135
+ determined.
136
+ """
137
+ if self._normalized_timestamp is None:
138
+ if self._number_of_seconds is not None:
139
+ self._normalized_timestamp = decimal.Decimal(self._number_of_seconds)
140
+
141
+ if self._time_zone_offset:
142
+ self._normalized_timestamp -= self._time_zone_offset * 60
143
+
144
+ return self._normalized_timestamp
145
+
146
+ def _CopyDateTimeFromStringISO8601(self, time_string):
147
+ """Copies a date and time from an ISO 8601 date and time string.
148
+
149
+ Args:
150
+ time_string (str): time value formatted as:
151
+ hh:mm:ss.######[+-]##:##
152
+
153
+ Where # are numeric digits ranging from 0 to 9 and the seconds
154
+ fraction can be either 3, 6 or 9 digits. The fraction of second and
155
+ time zone offset are optional.
156
+
157
+ Returns:
158
+ dict[str, int]: date and time values, such as year, month, day of month,
159
+ hours, minutes, seconds, nanoseconds, time zone offset in minutes.
160
+
161
+ Raises:
162
+ ValueError: if the time string is invalid or not supported.
163
+ """
164
+ if not time_string:
165
+ raise ValueError("Invalid time string.")
166
+
167
+ time_string_length = len(time_string)
168
+
169
+ year, month, day_of_month = self._CopyDateFromString(time_string)
170
+
171
+ if time_string_length <= 10:
172
+ return {"year": year, "month": month, "day_of_month": day_of_month}
173
+
174
+ # If a time of day is specified the time string it should at least
175
+ # contain 'YYYY-MM-DDThh'.
176
+ if time_string[10] != "T":
177
+ raise ValueError("Invalid time string - missing date and time separator.")
178
+
179
+ hours, minutes, seconds, nanoseconds, time_zone_offset = (
180
+ self._CopyTimeFromStringISO8601(time_string[11:])
181
+ )
182
+ date_time_values = {
183
+ "year": year,
184
+ "month": month,
185
+ "day_of_month": day_of_month,
186
+ "hours": hours,
187
+ "minutes": minutes,
188
+ "seconds": seconds,
189
+ }
190
+ if nanoseconds is not None:
191
+ date_time_values["nanoseconds"] = nanoseconds
192
+ if time_zone_offset is not None:
193
+ date_time_values["time_zone_offset"] = time_zone_offset
194
+
195
+ return date_time_values
196
+
197
+ def _CopyDateTimeFromStringRFC822(self, time_string):
198
+ """Copies a date and time from a RFC 822 date and time string.
199
+
200
+ Args:
201
+ time_string (str): date and time value formatted as:
202
+ DAY, D MONTH YY hh:mm:ss ZONE
203
+
204
+ Where weekday (DAY) and seconds (ss) are optional and day of
205
+ month (D) can consist of 1 or 2 digits.
206
+
207
+ Returns:
208
+ dict[str, int]: date and time values, such as year, month, day of month,
209
+ hours, minutes, seconds, time zone offset in minutes.
210
+
211
+ Raises:
212
+ ValueError: if the time string is invalid or not supported.
213
+ """
214
+ if not time_string:
215
+ raise ValueError("Invalid time string.")
216
+
217
+ string_segments = time_string.split(" ")
218
+
219
+ if len(string_segments) not in (5, 6):
220
+ raise ValueError("Unsupported number of time string segments.")
221
+
222
+ weekday_string = string_segments[0]
223
+ if weekday_string.endswith(","):
224
+ weekday_string = weekday_string[:-1]
225
+ if weekday_string not in self._RFC_WEEKDAYS:
226
+ raise ValueError(f"Invalid weekday: {weekday_string:s}.")
227
+
228
+ string_segments.pop(0)
229
+
230
+ day_of_month_string = string_segments[0]
231
+
232
+ day_of_month = 0
233
+ if len(day_of_month_string) in (1, 2):
234
+ try:
235
+ day_of_month = int(day_of_month_string, 10)
236
+ except ValueError:
237
+ pass
238
+
239
+ if day_of_month == 0:
240
+ raise ValueError(f"Invalid day of month: {day_of_month_string:s}.")
241
+
242
+ month_string = string_segments[1]
243
+
244
+ month = self._RFC_MONTH_MAPPINGS.get(month_string)
245
+ if not month:
246
+ raise ValueError(f"Invalid month: {month_string:s}.")
247
+
248
+ year_string = string_segments[2]
249
+
250
+ year = None
251
+ if len(year_string) == 2:
252
+ try:
253
+ year = int(year_string, 10)
254
+ except ValueError:
255
+ pass
256
+
257
+ if year is None:
258
+ raise ValueError(f"Invalid year: {0:s}.")
259
+
260
+ year += 1900
261
+
262
+ hours, minutes, seconds, time_zone_offset = self._CopyTimeFromStringRFC(
263
+ string_segments[3], string_segments[4]
264
+ )
265
+ date_time_values = {
266
+ "year": year,
267
+ "month": month,
268
+ "day_of_month": day_of_month,
269
+ "hours": hours,
270
+ "minutes": minutes,
271
+ "time_zone_offset": time_zone_offset,
272
+ }
273
+ if seconds is not None:
274
+ date_time_values["seconds"] = seconds
275
+
276
+ return date_time_values
277
+
278
+ def _CopyDateTimeFromStringRFC1123(self, time_string):
279
+ """Copies a date and time from a RFC 1123 date and time string.
280
+
281
+ Args:
282
+ time_string (str): date and time value formatted as:
283
+ DAY, D MONTH YYYY hh:mm:ss ZONE
284
+
285
+ Where weekday (DAY) and seconds (ss) are optional and day of
286
+ month (D) can consist of 1 or 2 digits.
287
+
288
+ Returns:
289
+ dict[str, int]: date and time values, such as year, month, day of month,
290
+ hours, minutes, seconds, time zone offset in minutes.
291
+
292
+ Raises:
293
+ ValueError: if the time string is invalid or not supported.
294
+ """
295
+ if not time_string:
296
+ raise ValueError("Invalid time string.")
297
+
298
+ string_segments = time_string.split(" ")
299
+
300
+ if len(string_segments) not in (5, 6):
301
+ raise ValueError("Unsupported number of time string segments.")
302
+
303
+ weekday_string = string_segments[0]
304
+ if weekday_string.endswith(","):
305
+ weekday_string = weekday_string[:-1]
306
+ if weekday_string not in self._RFC_WEEKDAYS:
307
+ raise ValueError(f"Invalid weekday: {weekday_string:s}.")
308
+
309
+ string_segments.pop(0)
310
+
311
+ day_of_month_string = string_segments[0]
312
+
313
+ day_of_month = 0
314
+ if len(day_of_month_string) in (1, 2):
315
+ try:
316
+ day_of_month = int(day_of_month_string, 10)
317
+ except ValueError:
318
+ pass
319
+
320
+ if day_of_month == 0:
321
+ raise ValueError(f"Invalid day of month: {day_of_month_string:s}.")
322
+
323
+ month_string = string_segments[1]
324
+
325
+ month = self._RFC_MONTH_MAPPINGS.get(month_string)
326
+ if not month:
327
+ raise ValueError(f"Invalid month: {month_string:s}.")
328
+
329
+ year_string = string_segments[2]
330
+
331
+ year = None
332
+ if len(year_string) == 4:
333
+ try:
334
+ year = int(year_string, 10)
335
+ except ValueError:
336
+ pass
337
+
338
+ if year is None:
339
+ raise ValueError(f"Invalid year: {year_string:s}.")
340
+
341
+ hours, minutes, seconds, time_zone_offset = self._CopyTimeFromStringRFC(
342
+ string_segments[3], string_segments[4]
343
+ )
344
+ date_time_values = {
345
+ "year": year,
346
+ "month": month,
347
+ "day_of_month": day_of_month,
348
+ "hours": hours,
349
+ "minutes": minutes,
350
+ "time_zone_offset": time_zone_offset,
351
+ }
352
+ if seconds is not None:
353
+ date_time_values["seconds"] = seconds
354
+
355
+ return date_time_values
356
+
357
+ def _CopyFromDateTimeValues(self, date_time_values):
358
+ """Copies time elements from date and time values.
359
+
360
+ Args:
361
+ date_time_values (dict[str, int]): date and time values, such as year,
362
+ month, day of month, hours, minutes, seconds, nanoseconds, time zone
363
+ offset in minutes.
364
+ """
365
+ year = date_time_values.get("year", 0)
366
+ month = date_time_values.get("month", 0)
367
+ day_of_month = date_time_values.get("day_of_month", 0)
368
+ hours = date_time_values.get("hours", 0)
369
+ minutes = date_time_values.get("minutes", 0)
370
+ seconds = date_time_values.get("seconds", 0)
371
+ time_zone_offset = date_time_values.get("time_zone_offset")
372
+
373
+ self._normalized_timestamp = None
374
+ self._number_of_seconds = self._GetNumberOfSecondsFromElements(
375
+ year, month, day_of_month, hours, minutes, seconds
376
+ )
377
+ self._time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)
378
+ self._time_zone_offset = time_zone_offset
379
+
380
+ def _CopyTimeFromStringISO8601(self, time_string):
381
+ """Copies a time from an ISO 8601 time string.
382
+
383
+ Args:
384
+ time_string (str): time value formatted as:
385
+ hh:mm:ss.######[+-]##:##
386
+
387
+ Where # are numeric digits ranging from 0 to 9 and the seconds
388
+ fraction can be either 3, 6 or 9 digits. The fraction of second and
389
+ time zone offset are optional.
390
+
391
+ Returns:
392
+ tuple[int, int, int, int, int]: hours, minutes, seconds, nanoseconds,
393
+ time zone offset in minutes.
394
+
395
+ Raises:
396
+ ValueError: if the time string is invalid or not supported.
397
+ """
398
+ if time_string.endswith("Z"):
399
+ time_string = "".join([time_string[:-1], "+00:00"])
400
+
401
+ time_string_length = len(time_string)
402
+
403
+ # The time string should at least contain 'hh'.
404
+ if time_string_length < 2:
405
+ raise ValueError("Time string too short.")
406
+
407
+ try:
408
+ hours = int(time_string[0:2], 10)
409
+ except ValueError:
410
+ raise ValueError("Unable to parse hours.")
411
+
412
+ if hours not in range(0, 24):
413
+ raise ValueError(f"Hours value: {hours:d} out of bounds.")
414
+
415
+ minutes = None
416
+ seconds = None
417
+ nanoseconds = None
418
+ time_zone_offset = None
419
+
420
+ time_string_index = 2
421
+
422
+ # Minutes are either specified as 'hhmm', 'hh:mm' or as a fractional part
423
+ # 'hh[.,]###'.
424
+ if time_string_index + 1 < time_string_length and time_string[
425
+ time_string_index
426
+ ] not in (".", ","):
427
+ if time_string[time_string_index] == ":":
428
+ time_string_index += 1
429
+
430
+ if time_string_index + 2 > time_string_length:
431
+ raise ValueError("Time string too short.")
432
+
433
+ try:
434
+ minutes = time_string[time_string_index : time_string_index + 2]
435
+ minutes = int(minutes, 10)
436
+ except ValueError:
437
+ raise ValueError("Unable to parse minutes.")
438
+
439
+ time_string_index += 2
440
+
441
+ # Seconds are either specified as 'hhmmss', 'hh:mm:ss' or as a fractional
442
+ # part 'hh:mm[.,]###' or 'hhmm[.,]###'.
443
+ if time_string_index + 1 < time_string_length and time_string[
444
+ time_string_index
445
+ ] not in (".", ","):
446
+ if time_string[time_string_index] == ":":
447
+ time_string_index += 1
448
+
449
+ if time_string_index + 2 > time_string_length:
450
+ raise ValueError("Time string too short.")
451
+
452
+ try:
453
+ seconds = time_string[time_string_index : time_string_index + 2]
454
+ seconds = int(seconds, 10)
455
+ except ValueError:
456
+ raise ValueError("Unable to parse day of seconds.")
457
+
458
+ time_string_index += 2
459
+
460
+ time_zone_string_index = time_string_index
461
+ while time_zone_string_index < time_string_length:
462
+ if time_string[time_zone_string_index] in ("+", "-"):
463
+ break
464
+
465
+ time_zone_string_index += 1
466
+
467
+ # The calculations that follow rely on the time zone string index
468
+ # to point beyond the string in case no time zone offset was defined.
469
+ if time_zone_string_index == time_string_length - 1:
470
+ time_zone_string_index += 1
471
+
472
+ if time_string_length > time_string_index and time_string[
473
+ time_string_index
474
+ ] in (".", ","):
475
+ time_string_index += 1
476
+ time_fraction_length = time_zone_string_index - time_string_index
477
+
478
+ try:
479
+ time_fraction = time_string[time_string_index:time_zone_string_index]
480
+ time_fraction = int(time_fraction, 10)
481
+ time_fraction = decimal.Decimal(time_fraction) / decimal.Decimal(
482
+ 10**time_fraction_length
483
+ )
484
+ except ValueError:
485
+ raise ValueError("Unable to parse time fraction.")
486
+
487
+ if minutes is None:
488
+ time_fraction *= 60
489
+ minutes = int(time_fraction)
490
+ time_fraction -= minutes
491
+
492
+ if seconds is None:
493
+ time_fraction *= 60
494
+ seconds = int(time_fraction)
495
+ time_fraction -= seconds
496
+
497
+ time_fraction *= definitions.NANOSECONDS_PER_SECOND
498
+ nanoseconds = int(time_fraction)
499
+
500
+ if minutes is not None and minutes not in range(0, 60):
501
+ raise ValueError(f"Minutes value: {minutes:d} out of bounds.")
502
+
503
+ # TODO: support a leap second?
504
+ if seconds is not None and seconds not in range(0, 60):
505
+ raise ValueError(f"Seconds value: {seconds:d} out of bounds.")
506
+
507
+ if time_zone_string_index < time_string_length:
508
+ if (
509
+ time_string_length - time_zone_string_index != 6
510
+ or time_string[time_zone_string_index + 3] != ":"
511
+ ):
512
+ raise ValueError("Invalid time string.")
513
+
514
+ try:
515
+ hours_from_utc = int(
516
+ time_string[time_zone_string_index + 1 : time_zone_string_index + 3]
517
+ )
518
+ except ValueError:
519
+ raise ValueError("Unable to parse time zone hours offset.")
520
+
521
+ if hours_from_utc not in range(0, 15):
522
+ raise ValueError("Time zone hours offset value out of bounds.")
523
+
524
+ try:
525
+ minutes_from_utc = int(
526
+ time_string[time_zone_string_index + 4 : time_zone_string_index + 6]
527
+ )
528
+ except ValueError:
529
+ raise ValueError("Unable to parse time zone minutes offset.")
530
+
531
+ if minutes_from_utc not in range(0, 60):
532
+ raise ValueError("Time zone minutes offset value out of bounds.")
533
+
534
+ # pylint: disable=invalid-unary-operand-type
535
+ time_zone_offset = (hours_from_utc * 60) + minutes_from_utc
536
+
537
+ if time_string[time_zone_string_index] == "-":
538
+ time_zone_offset = -time_zone_offset
539
+
540
+ return hours, minutes, seconds, nanoseconds, time_zone_offset
541
+
542
+ def _CopyTimeFromStringRFC(self, time_string, time_zone_string):
543
+ """Copies a time from a RFC 822, RFC 1123 or RFC 2822 time string.
544
+
545
+ Args:
546
+ time_string (str): time value formatted as: hh:mm[:ss], where seconds (ss)
547
+ are optional.
548
+ time_zone_string (str): time zone value formatted as predefined time zone
549
+ indicator or [+-]HHMM
550
+
551
+ Returns:
552
+ tuple[int, int, int, int]: hours, minutes, seconds, time zone offset in
553
+ minutes.
554
+
555
+ Raises:
556
+ ValueError: if the time string is invalid or not supported.
557
+ """
558
+ time_string_length = len(time_string)
559
+
560
+ # The time string should at least contain 'hh:mm'.
561
+ if time_string_length < 5:
562
+ raise ValueError("Time string too short.")
563
+
564
+ if time_string_length > 8:
565
+ raise ValueError("Time string too long.")
566
+
567
+ if time_string[2] != ":":
568
+ raise ValueError("Invalid hours and minutes separator.")
569
+
570
+ try:
571
+ hours = int(time_string[0:2], 10)
572
+ except ValueError:
573
+ raise ValueError("Unable to parse hours.")
574
+
575
+ if hours not in range(0, 24):
576
+ raise ValueError(f"Hours value: {hours:d} out of bounds.")
577
+
578
+ try:
579
+ minutes = int(time_string[3:5], 10)
580
+ except ValueError:
581
+ raise ValueError("Unable to parse minutes.")
582
+
583
+ if minutes not in range(0, 60):
584
+ raise ValueError(f"Minutes value: {minutes:d} out of bounds.")
585
+
586
+ seconds = None
587
+
588
+ if time_string_length > 5:
589
+ if time_string_length < 8:
590
+ raise ValueError("Time string too short.")
591
+
592
+ if time_string[5] != ":":
593
+ raise ValueError("Invalid minutes and seconds separator.")
594
+
595
+ try:
596
+ seconds = int(time_string[6:8], 10)
597
+ except ValueError:
598
+ raise ValueError("Unable to parse seconds.")
599
+
600
+ if seconds not in range(0, 60):
601
+ raise ValueError(f"Seconds value: {seconds:d} out of bounds.")
602
+
603
+ time_zone_string_length = len(time_zone_string)
604
+ if time_zone_string_length > 5:
605
+ raise ValueError("Time zone string too long.")
606
+
607
+ if time_zone_string_length < 5:
608
+ hours_from_utc = self._RFC_TIME_ZONE_MAPPINGS.get(time_zone_string)
609
+ minutes_from_utc = 0
610
+ if hours_from_utc is None:
611
+ raise ValueError(f"Invalid time zone: {time_zone_string:s}.")
612
+
613
+ else:
614
+ if time_zone_string[0] not in ("+", "-"):
615
+ raise ValueError(f"Invalid time zone: {time_zone_string:s}.")
616
+
617
+ try:
618
+ hours_from_utc = int(time_zone_string[1:3], 10)
619
+ except ValueError:
620
+ raise ValueError("Unable to parse time zone hours offset.")
621
+
622
+ if hours_from_utc not in range(0, 15):
623
+ raise ValueError("Time zone hours offset value out of bounds.")
624
+
625
+ try:
626
+ minutes_from_utc = int(time_zone_string[3:5], 10)
627
+ except ValueError:
628
+ raise ValueError("Unable to parse time zone minutes offset.")
629
+
630
+ if minutes_from_utc not in range(0, 60):
631
+ raise ValueError("Time zone minutes offset value out of bounds.")
632
+
633
+ time_zone_offset = (hours_from_utc * 60) + minutes_from_utc
634
+ if time_zone_string[0] == "-":
635
+ time_zone_offset = -time_zone_offset
636
+
637
+ return hours, minutes, seconds, time_zone_offset
638
+
639
+ @property
640
+ def day_of_month(self):
641
+ """int: day of month or None if not set."""
642
+ if not self._time_elements_tuple:
643
+ return None
644
+ return self._time_elements_tuple[2]
645
+
646
+ @property
647
+ def hours(self):
648
+ """int: number of hours or None if not set."""
649
+ if not self._time_elements_tuple:
650
+ return None
651
+ return self._time_elements_tuple[3]
652
+
653
+ @property
654
+ def minutes(self):
655
+ """int: number of minutes or None if not set."""
656
+ if not self._time_elements_tuple:
657
+ return None
658
+ return self._time_elements_tuple[4]
659
+
660
+ @property
661
+ def month(self):
662
+ """int: month or None if not set."""
663
+ if not self._time_elements_tuple:
664
+ return None
665
+ return self._time_elements_tuple[1]
666
+
667
+ @property
668
+ def seconds(self):
669
+ """int: number of seconds or None if not set."""
670
+ if not self._time_elements_tuple:
671
+ return None
672
+ return self._time_elements_tuple[5]
673
+
674
+ @property
675
+ def year(self):
676
+ """int: year or None if not set."""
677
+ if not self._time_elements_tuple:
678
+ return None
679
+ return self._time_elements_tuple[0]
680
+
681
+ def CopyFromDatetime(self, datetime_object):
682
+ """Copies time elements from a Python datetime object.
683
+
684
+ A naive datetime object is considered in local time.
685
+
686
+ Args:
687
+ datetime_object (datetime.datetime): Python datetime object.
688
+ """
689
+ year, month, day_of_month, hours, minutes, seconds, _, _, _ = (
690
+ datetime_object.utctimetuple()
691
+ )
692
+ date_time_values = {
693
+ "year": year,
694
+ "month": month,
695
+ "day_of_month": day_of_month,
696
+ "hours": hours,
697
+ "minutes": minutes,
698
+ "seconds": seconds,
699
+ }
700
+ self._CopyFromDateTimeValues(date_time_values)
701
+
702
+ self.is_local_time = bool(datetime_object.tzinfo is None)
703
+
704
+ def CopyFromDateTimeString(self, time_string):
705
+ """Copies time elements from a date and time string.
706
+
707
+ Args:
708
+ time_string (str): date and time value formatted as:
709
+ YYYY-MM-DD hh:mm:ss.######[+-]##:##
710
+
711
+ Where # are numeric digits ranging from 0 to 9 and the seconds
712
+ fraction can be either 3, 6 or 9 digits. The time of day, seconds
713
+ fraction and time zone offset are optional. The default time zone
714
+ is UTC.
715
+ """
716
+ date_time_values = self._CopyDateTimeFromString(time_string)
717
+
718
+ self._CopyFromDateTimeValues(date_time_values)
719
+
720
+ def CopyFromStringISO8601(self, time_string):
721
+ """Copies time elements from an ISO 8601 date and time string.
722
+
723
+ Currently not supported:
724
+ * Duration notation: "P..."
725
+ * Week notation "2016-W33"
726
+ * Date with week number notation "2016-W33-3"
727
+ * Date without year notation "--08-17"
728
+ * Ordinal date notation "2016-230"
729
+
730
+ Args:
731
+ time_string (str): date and time value formatted as:
732
+ YYYY-MM-DDThh:mm:ss.######[+-]##:##
733
+
734
+ Where # are numeric digits ranging from 0 to 9 and the seconds
735
+ fraction can be either 3, 6 or 9 digits. The time of day, seconds
736
+ fraction and time zone offset are optional. The default time zone
737
+ is UTC.
738
+
739
+ Raises:
740
+ ValueError: if the time string is invalid or not supported.
741
+ """
742
+ date_time_values = self._CopyDateTimeFromStringISO8601(time_string)
743
+
744
+ self._CopyFromDateTimeValues(date_time_values)
745
+
746
+ def CopyFromStringRFC822(self, time_string):
747
+ """Copies time elements from a RFC 822 date and time string.
748
+
749
+ Args:
750
+ time_string (str): date and time value formatted as:
751
+ DAY, D MONTH YY hh:mm:ss ZONE
752
+
753
+ Where weekday (DAY) and seconds (ss) are optional and day of
754
+ month (D) can consist of 1 or 2 digits.
755
+
756
+ Raises:
757
+ ValueError: if the time string is invalid or not supported.
758
+ """
759
+ date_time_values = self._CopyDateTimeFromStringRFC822(time_string)
760
+
761
+ self._CopyFromDateTimeValues(date_time_values)
762
+
763
+ def CopyFromStringRFC1123(self, time_string):
764
+ """Copies time elements from a RFC 1123 date and time string.
765
+
766
+ Args:
767
+ time_string (str): date and time value formatted as:
768
+ DAY, D MONTH YYYY hh:mm:ss ZONE
769
+
770
+ Where weekday (DAY) and seconds (ss) are optional and day of
771
+ month (D) can consist of 1 or 2 digits.
772
+
773
+ Raises:
774
+ ValueError: if the time string is invalid or not supported.
775
+ """
776
+ date_time_values = self._CopyDateTimeFromStringRFC1123(time_string)
777
+
778
+ self._CopyFromDateTimeValues(date_time_values)
779
+
780
+ def CopyFromStringTuple(self, time_elements_tuple):
781
+ """Copies time elements from string-based time elements tuple.
782
+
783
+ Args:
784
+ time_elements_tuple (Optional[tuple[str, str, str, str, str, str]]):
785
+ time elements, contains year, month, day of month, hours, minutes and
786
+ seconds.
787
+
788
+ Raises:
789
+ ValueError: if the time elements tuple is invalid.
790
+ """
791
+ number_of_elements = len(time_elements_tuple)
792
+ if number_of_elements < 6:
793
+ raise ValueError(
794
+ (
795
+ f"Invalid time elements tuple at least 6 elements required,"
796
+ f"got: {number_of_elements:d}"
797
+ )
798
+ )
799
+
800
+ year_string = time_elements_tuple[0]
801
+ month_string = time_elements_tuple[1]
802
+ day_of_month_string = time_elements_tuple[2]
803
+ hours_string = time_elements_tuple[3]
804
+ minutes_string = time_elements_tuple[4]
805
+ seconds_string = time_elements_tuple[5]
806
+
807
+ try:
808
+ year = int(year_string, 10)
809
+ except (TypeError, ValueError):
810
+ raise ValueError(f"Invalid year value: {year_string!s}")
811
+
812
+ try:
813
+ month = int(month_string, 10)
814
+ except (TypeError, ValueError):
815
+ raise ValueError(f"Invalid month value: {month_string!s}")
816
+
817
+ try:
818
+ day_of_month = int(day_of_month_string, 10)
819
+ except (TypeError, ValueError):
820
+ raise ValueError(f"Invalid day of month value: {day_of_month_string!s}")
821
+
822
+ try:
823
+ hours = int(hours_string, 10)
824
+ except (TypeError, ValueError):
825
+ raise ValueError(f"Invalid hours value: {hours_string!s}")
826
+
827
+ try:
828
+ minutes = int(minutes_string, 10)
829
+ except (TypeError, ValueError):
830
+ raise ValueError(f"Invalid minutes value: {minutes_string!s}")
831
+
832
+ try:
833
+ seconds = int(seconds_string, 10)
834
+ except (TypeError, ValueError):
835
+ raise ValueError(f"Invalid seconds value: {seconds_string!s}")
836
+
837
+ self._normalized_timestamp = None
838
+ self._number_of_seconds = self._GetNumberOfSecondsFromElements(
839
+ year, month, day_of_month, hours, minutes, seconds
840
+ )
841
+ self._time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)
842
+
843
+ def CopyToDateTimeString(self):
844
+ """Copies the time elements to a date and time string.
845
+
846
+ Returns:
847
+ str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss" or None
848
+ if time elements are missing.
849
+ """
850
+ if self._number_of_seconds is None:
851
+ return None
852
+
853
+ year, month, day_of_month, hours, minutes, seconds = self._time_elements_tuple
854
+
855
+ return (
856
+ f"{year:04d}-{month:02d}-{day_of_month:02d} "
857
+ f"{hours:02d}:{minutes:02d}:{seconds:02d}"
858
+ )
859
+
860
+ def CopyToSerializableDict(self):
861
+ """Copies the date time value to a serializable dictionary.
862
+
863
+ Returns:
864
+ dict[str, object]: serializable dictionary.
865
+ """
866
+ serializable_dict = self._CreateSerializableDict()
867
+
868
+ serializable_dict["time_elements_tuple"] = self._time_elements_tuple
869
+
870
+ if self._is_delta:
871
+ serializable_dict["is_delta"] = True
872
+
873
+ return serializable_dict
874
+
875
+ def NewFromDeltaAndDate(self, year, month, day_of_month):
876
+ """Creates a new time elements instance from a date time delta and a date.
877
+
878
+ Args:
879
+ year (int): year.
880
+ month (int): month, where 1 represents January and 0 if not set.
881
+ day_of_month (int): day of month, where 1 represents the first day and 0
882
+ if not set.
883
+
884
+ Returns:
885
+ TimeElements: time elements or None if time elements are missing.
886
+
887
+ Raises:
888
+ ValueError: if the instance is not a date time delta.
889
+ """
890
+ if not self._is_delta:
891
+ raise ValueError("Not a date time delta.")
892
+
893
+ if self._time_elements_tuple is None:
894
+ return None
895
+
896
+ delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
897
+ self._time_elements_tuple
898
+ )
899
+ time_elements_tuple = (
900
+ year + delta_year,
901
+ month + delta_month,
902
+ day_of_month + delta_day_of_month,
903
+ hours,
904
+ minutes,
905
+ seconds,
906
+ )
907
+ date_time = TimeElements(
908
+ precision=self._precision,
909
+ time_elements_tuple=time_elements_tuple,
910
+ time_zone_offset=self._time_zone_offset,
911
+ )
912
+ date_time.is_local_time = self.is_local_time
913
+
914
+ return date_time
915
+
916
+ def NewFromDeltaAndYear(self, year):
917
+ """Creates a new time elements instance from a date time delta and a year.
918
+
919
+ Args:
920
+ year (int): year.
921
+
922
+ Returns:
923
+ TimeElements: time elements or None if time elements are missing.
924
+
925
+ Raises:
926
+ ValueError: if the instance is not a date time delta.
927
+ """
928
+ return self.NewFromDeltaAndDate(year, 0, 0)
929
+
930
+
931
+ class TimeElementsWithFractionOfSecond(TimeElements):
932
+ """Time elements with a fraction of second interface.
933
+
934
+ Attributes:
935
+ fraction_of_second (decimal.Decimal): fraction of second, which must be a
936
+ value between 0.0 and 1.0.
937
+ is_local_time (bool): True if the date and time value is in local time.
938
+ """
939
+
940
+ def __init__(
941
+ self,
942
+ fraction_of_second=None,
943
+ is_delta=False,
944
+ precision=None,
945
+ time_elements_tuple=None,
946
+ time_zone_offset=None,
947
+ ):
948
+ """Initializes time elements.
949
+
950
+ Args:
951
+ fraction_of_second (Optional[decimal.Decimal]): fraction of second, which
952
+ must be a value between 0.0 and 1.0.
953
+ is_delta (Optional[bool]): True if the date and time value is relative to
954
+ another date and time value.
955
+ precision (Optional[str]): precision of the date and time value, which
956
+ should be one of the PRECISION_VALUES in definitions.
957
+ time_elements_tuple (Optional[tuple[int, int, int, int, int, int]]):
958
+ time elements, contains year, month, day of month, hours, minutes and
959
+ seconds.
960
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
961
+ from UTC or None if not set.
962
+
963
+ Raises:
964
+ ValueError: if the time elements tuple is invalid or fraction of second
965
+ value is out of bounds.
966
+ """
967
+ if fraction_of_second is not None:
968
+ if fraction_of_second < 0.0 or fraction_of_second >= 1.0:
969
+ raise ValueError(
970
+ f"Fraction of second value: {fraction_of_second:f} out of bounds."
971
+ )
972
+
973
+ super().__init__(
974
+ is_delta=is_delta,
975
+ precision=precision or definitions.PRECISION_1_SECOND,
976
+ time_elements_tuple=time_elements_tuple,
977
+ time_zone_offset=time_zone_offset,
978
+ )
979
+ self.fraction_of_second = fraction_of_second
980
+
981
+ def _GetNormalizedTimestamp(self):
982
+ """Retrieves the normalized timestamp.
983
+
984
+ Returns:
985
+ decimal.Decimal: normalized timestamp, which contains the number of
986
+ seconds since January 1, 1970 00:00:00 and a fraction of second used
987
+ for increased precision, or None if the normalized timestamp cannot be
988
+ determined.
989
+ """
990
+ if self._normalized_timestamp is None:
991
+ if (
992
+ self._number_of_seconds is not None
993
+ and self.fraction_of_second is not None
994
+ ):
995
+ self._normalized_timestamp = (
996
+ decimal.Decimal(self._number_of_seconds) + self.fraction_of_second
997
+ )
998
+ if self._time_zone_offset:
999
+ self._normalized_timestamp -= self._time_zone_offset * 60
1000
+
1001
+ return self._normalized_timestamp
1002
+
1003
+ def _CopyFromDateTimeValues(self, date_time_values):
1004
+ """Copies time elements from date and time values.
1005
+
1006
+ Args:
1007
+ date_time_values (dict[str, int]): date and time values, such as year,
1008
+ month, day of month, hours, minutes, seconds, nanoseconds, time zone
1009
+ offset in minutes.
1010
+
1011
+ Raises:
1012
+ ValueError: if no helper can be created for the current precision.
1013
+ """
1014
+ year = date_time_values.get("year", 0)
1015
+ month = date_time_values.get("month", 0)
1016
+ day_of_month = date_time_values.get("day_of_month", 0)
1017
+ hours = date_time_values.get("hours", 0)
1018
+ minutes = date_time_values.get("minutes", 0)
1019
+ seconds = date_time_values.get("seconds", 0)
1020
+ nanoseconds = date_time_values.get("nanoseconds", 0)
1021
+ time_zone_offset = date_time_values.get("time_zone_offset")
1022
+
1023
+ precision_helper = precisions.PrecisionHelperFactory.CreatePrecisionHelper(
1024
+ self._precision
1025
+ )
1026
+ fraction_of_second = precision_helper.CopyNanosecondsToFractionOfSecond(
1027
+ nanoseconds
1028
+ )
1029
+ self._normalized_timestamp = None
1030
+ self._number_of_seconds = self._GetNumberOfSecondsFromElements(
1031
+ year, month, day_of_month, hours, minutes, seconds
1032
+ )
1033
+ self._time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)
1034
+ self._time_zone_offset = time_zone_offset
1035
+
1036
+ self.fraction_of_second = fraction_of_second
1037
+
1038
+ def CopyFromDatetime(self, datetime_object):
1039
+ """Copies time elements from a Python datetime object.
1040
+
1041
+ A naive datetime object is considered in local time.
1042
+
1043
+ Args:
1044
+ datetime_object (datetime.datetime): Python datetime object.
1045
+ """
1046
+ super().CopyFromDatetime(datetime_object)
1047
+
1048
+ precision_helper = precisions.PrecisionHelperFactory.CreatePrecisionHelper(
1049
+ self._precision
1050
+ )
1051
+ fraction_of_second = precision_helper.CopyNanosecondsToFractionOfSecond(
1052
+ datetime_object.microsecond * definitions.NANOSECONDS_PER_MICROSECOND
1053
+ )
1054
+ self.fraction_of_second = fraction_of_second
1055
+
1056
+ def CopyFromStringTuple(self, time_elements_tuple):
1057
+ """Copies time elements from string-based time elements tuple.
1058
+
1059
+ Args:
1060
+ time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
1061
+ time elements, contains year, month, day of month, hours, minutes,
1062
+ seconds and fraction of seconds.
1063
+
1064
+ Raises:
1065
+ ValueError: if the time elements tuple is invalid.
1066
+ """
1067
+ number_of_elements = len(time_elements_tuple)
1068
+ if number_of_elements < 7:
1069
+ raise ValueError(
1070
+ f"Invalid time elements tuple at least 7 elements required,"
1071
+ f"got: {number_of_elements:d}"
1072
+ )
1073
+
1074
+ super().CopyFromStringTuple(time_elements_tuple)
1075
+
1076
+ fraction_of_second_string = time_elements_tuple[6]
1077
+
1078
+ try:
1079
+ fraction_of_second = decimal.Decimal(fraction_of_second_string)
1080
+ except (TypeError, ValueError):
1081
+ raise ValueError(
1082
+ f"Invalid fraction of second value: {fraction_of_second_string!s}"
1083
+ )
1084
+
1085
+ if fraction_of_second < 0.0 or fraction_of_second >= 1.0:
1086
+ raise ValueError(
1087
+ f"Fraction of second value: {fraction_of_second:f} out of bounds."
1088
+ )
1089
+
1090
+ self.fraction_of_second = fraction_of_second
1091
+
1092
+ def CopyToDateTimeString(self):
1093
+ """Copies the time elements to a date and time string.
1094
+
1095
+ Returns:
1096
+ str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss" or
1097
+ "YYYY-MM-DD hh:mm:ss.######" or None if time elements are missing.
1098
+
1099
+ Raises:
1100
+ ValueError: if the precision value is unsupported.
1101
+ """
1102
+ if self._number_of_seconds is None or self.fraction_of_second is None:
1103
+ return None
1104
+
1105
+ precision_helper = precisions.PrecisionHelperFactory.CreatePrecisionHelper(
1106
+ self._precision
1107
+ )
1108
+ return precision_helper.CopyToDateTimeString(
1109
+ self._time_elements_tuple, self.fraction_of_second
1110
+ )
1111
+
1112
+ def NewFromDeltaAndDate(self, year, month, day_of_month):
1113
+ """Creates a new time elements instance from a date time delta and a date.
1114
+
1115
+ Args:
1116
+ year (int): year.
1117
+ month (int): month, where 1 represents January and 0 if not set.
1118
+ day_of_month (int): day of month, where 1 represents the first day and 0
1119
+ if not set.
1120
+
1121
+ Returns:
1122
+ TimeElementsWithFractionOfSecond: time elements or None if time elements
1123
+ are missing.
1124
+
1125
+ Raises:
1126
+ ValueError: if the instance is not a date time delta.
1127
+ """
1128
+ if not self._is_delta:
1129
+ raise ValueError("Not a date time delta.")
1130
+
1131
+ if self._time_elements_tuple is None:
1132
+ return None
1133
+
1134
+ delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
1135
+ self._time_elements_tuple
1136
+ )
1137
+ time_elements_tuple = (
1138
+ year + delta_year,
1139
+ month + delta_month,
1140
+ day_of_month + delta_day_of_month,
1141
+ hours,
1142
+ minutes,
1143
+ seconds,
1144
+ )
1145
+ return TimeElementsWithFractionOfSecond(
1146
+ fraction_of_second=self.fraction_of_second,
1147
+ precision=self._precision,
1148
+ time_elements_tuple=time_elements_tuple,
1149
+ time_zone_offset=self._time_zone_offset,
1150
+ )
1151
+
1152
+ def NewFromDeltaAndYear(self, year):
1153
+ """Creates a new time elements instance from a date time delta and a year.
1154
+
1155
+ Args:
1156
+ year (int): year.
1157
+
1158
+ Returns:
1159
+ TimeElementsWithFractionOfSecond: time elements or None if time elements
1160
+ are missing.
1161
+
1162
+ Raises:
1163
+ ValueError: if the instance is not a date time delta.
1164
+ """
1165
+ return self.NewFromDeltaAndDate(year, 0, 0)
1166
+
1167
+
1168
+ class TimeElementsInMilliseconds(TimeElementsWithFractionOfSecond):
1169
+ """Time elements in milliseconds.
1170
+
1171
+ Attributes:
1172
+ fraction_of_second (decimal.Decimal): fraction of second, which must be a
1173
+ value between 0.0 and 1.0.
1174
+ is_local_time (bool): True if the date and time value is in local time.
1175
+ precision (str): precision of the date of the date and time value, that
1176
+ represents 1 millisecond (PRECISION_1_MILLISECOND).
1177
+ """
1178
+
1179
+ def __init__(
1180
+ self,
1181
+ is_delta=False,
1182
+ precision=None,
1183
+ time_elements_tuple=None,
1184
+ time_zone_offset=None,
1185
+ ):
1186
+ """Initializes time elements.
1187
+
1188
+ Args:
1189
+ is_delta (Optional[bool]): True if the date and time value is relative to
1190
+ another date and time value.
1191
+ precision (Optional[str]): precision of the date and time value, which
1192
+ should be one of the PRECISION_VALUES in definitions.
1193
+ time_elements_tuple (Optional[tuple[int, int, int, int, int, int, int]]):
1194
+ time elements, contains year, month, day of month, hours, minutes,
1195
+ seconds and milliseconds.
1196
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
1197
+ from UTC or None if not set.
1198
+
1199
+ Raises:
1200
+ ValueError: if the time elements tuple is invalid.
1201
+ """
1202
+ fraction_of_second = None
1203
+ if time_elements_tuple:
1204
+ number_of_elements = len(time_elements_tuple)
1205
+ if number_of_elements < 7:
1206
+ raise ValueError(
1207
+ (
1208
+ f"Invalid time elements tuple at least 7 elements required,"
1209
+ f"got: {number_of_elements:d}"
1210
+ )
1211
+ )
1212
+
1213
+ milliseconds = time_elements_tuple[6]
1214
+ time_elements_tuple = time_elements_tuple[:6]
1215
+
1216
+ if milliseconds < 0 or milliseconds >= definitions.MILLISECONDS_PER_SECOND:
1217
+ raise ValueError("Invalid number of milliseconds.")
1218
+
1219
+ fraction_of_second = (
1220
+ decimal.Decimal(milliseconds) / definitions.MILLISECONDS_PER_SECOND
1221
+ )
1222
+
1223
+ super().__init__(
1224
+ fraction_of_second=fraction_of_second,
1225
+ is_delta=is_delta,
1226
+ precision=precision or definitions.PRECISION_1_MILLISECOND,
1227
+ time_elements_tuple=time_elements_tuple,
1228
+ time_zone_offset=time_zone_offset,
1229
+ )
1230
+
1231
+ @property
1232
+ def milliseconds(self):
1233
+ """int: number of milliseconds."""
1234
+ return int(self.fraction_of_second * definitions.MILLISECONDS_PER_SECOND)
1235
+
1236
+ def CopyFromStringTuple(self, time_elements_tuple):
1237
+ """Copies time elements from string-based time elements tuple.
1238
+
1239
+ Args:
1240
+ time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
1241
+ time elements, contains year, month, day of month, hours, minutes,
1242
+ seconds and milliseconds.
1243
+
1244
+ Raises:
1245
+ ValueError: if the time elements tuple is invalid.
1246
+ """
1247
+ number_of_elements = len(time_elements_tuple)
1248
+ if len(time_elements_tuple) < 7:
1249
+ raise ValueError(
1250
+ (
1251
+ f"Invalid time elements tuple at least 7 elements required,"
1252
+ f"got: {number_of_elements:d}"
1253
+ )
1254
+ )
1255
+
1256
+ year, month, day_of_month, hours, minutes, seconds, milliseconds = (
1257
+ time_elements_tuple
1258
+ )
1259
+ try:
1260
+ milliseconds = int(milliseconds, 10)
1261
+ except (TypeError, ValueError):
1262
+ raise ValueError(f"Invalid millisecond value: {milliseconds!s}")
1263
+
1264
+ if milliseconds < 0 or milliseconds >= definitions.MILLISECONDS_PER_SECOND:
1265
+ raise ValueError("Invalid number of milliseconds.")
1266
+
1267
+ fraction_of_second = (
1268
+ decimal.Decimal(milliseconds) / definitions.MILLISECONDS_PER_SECOND
1269
+ )
1270
+ time_elements_tuple = (
1271
+ year,
1272
+ month,
1273
+ day_of_month,
1274
+ hours,
1275
+ minutes,
1276
+ seconds,
1277
+ str(fraction_of_second),
1278
+ )
1279
+ super().CopyFromStringTuple(time_elements_tuple)
1280
+
1281
+ def CopyToSerializableDict(self):
1282
+ """Copies the date time value to a serializable dictionary.
1283
+
1284
+ Returns:
1285
+ dict[str, object]: serializable dictionary.
1286
+ """
1287
+ serializable_dict = self._CreateSerializableDict()
1288
+
1289
+ serializable_dict["time_elements_tuple"] = (
1290
+ *self._time_elements_tuple,
1291
+ self.milliseconds,
1292
+ )
1293
+
1294
+ if self._is_delta:
1295
+ serializable_dict["is_delta"] = True
1296
+
1297
+ return serializable_dict
1298
+
1299
+ def NewFromDeltaAndDate(self, year, month, day_of_month):
1300
+ """Creates a new time elements instance from a date time delta and a date.
1301
+
1302
+ Args:
1303
+ year (int): year.
1304
+ month (int): month, where 1 represents January and 0 if not set.
1305
+ day_of_month (int): day of month, where 1 represents the first day and 0
1306
+ if not set.
1307
+
1308
+ Returns:
1309
+ TimeElementsInMilliseconds: time elements or None if time elements are
1310
+ missing.
1311
+
1312
+ Raises:
1313
+ ValueError: if the instance is not a date time delta.
1314
+ """
1315
+ if not self._is_delta:
1316
+ raise ValueError("Not a date time delta.")
1317
+
1318
+ if self._time_elements_tuple is None:
1319
+ return None
1320
+
1321
+ delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
1322
+ self._time_elements_tuple
1323
+ )
1324
+ time_elements_tuple = (
1325
+ year + delta_year,
1326
+ month + delta_month,
1327
+ day_of_month + delta_day_of_month,
1328
+ hours,
1329
+ minutes,
1330
+ seconds,
1331
+ self.milliseconds,
1332
+ )
1333
+ return TimeElementsInMilliseconds(
1334
+ precision=self._precision,
1335
+ time_elements_tuple=time_elements_tuple,
1336
+ time_zone_offset=self._time_zone_offset,
1337
+ )
1338
+
1339
+ def NewFromDeltaAndYear(self, year):
1340
+ """Creates a new time elements instance from a date time delta and a year.
1341
+
1342
+ Args:
1343
+ year (int): year.
1344
+
1345
+ Returns:
1346
+ TimeElementsInMilliseconds: time elements or None if time elements are
1347
+ missing.
1348
+
1349
+ Raises:
1350
+ ValueError: if the instance is not a date time delta.
1351
+ """
1352
+ return self.NewFromDeltaAndDate(year, 0, 0)
1353
+
1354
+
1355
+ class TimeElementsInMicroseconds(TimeElementsWithFractionOfSecond):
1356
+ """Time elements in microseconds.
1357
+
1358
+ Attributes:
1359
+ fraction_of_second (decimal.Decimal): fraction of second, which must be a
1360
+ value between 0.0 and 1.0.
1361
+ is_local_time (bool): True if the date and time value is in local time.
1362
+ precision (str): precision of the date of the date and time value, that
1363
+ represents 1 microsecond (PRECISION_1_MICROSECOND).
1364
+ """
1365
+
1366
+ def __init__(
1367
+ self,
1368
+ is_delta=False,
1369
+ precision=None,
1370
+ time_elements_tuple=None,
1371
+ time_zone_offset=None,
1372
+ ):
1373
+ """Initializes time elements.
1374
+
1375
+ Args:
1376
+ is_delta (Optional[bool]): True if the date and time value is relative to
1377
+ another date and time value.
1378
+ precision (Optional[str]): precision of the date and time value, which
1379
+ should be one of the PRECISION_VALUES in definitions.
1380
+ time_elements_tuple (Optional[tuple[int, int, int, int, int, int, int]]):
1381
+ time elements, contains year, month, day of month, hours, minutes,
1382
+ seconds and microseconds.
1383
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
1384
+ from UTC or None if not set.
1385
+
1386
+ Raises:
1387
+ ValueError: if the time elements tuple is invalid.
1388
+ """
1389
+ fraction_of_second = None
1390
+ if time_elements_tuple:
1391
+ number_of_elements = len(time_elements_tuple)
1392
+ if number_of_elements < 7:
1393
+ raise ValueError(
1394
+ (
1395
+ f"Invalid time elements tuple at least 7 elements required,"
1396
+ f"got: {number_of_elements:d}"
1397
+ )
1398
+ )
1399
+
1400
+ microseconds = time_elements_tuple[6]
1401
+ time_elements_tuple = time_elements_tuple[:6]
1402
+
1403
+ if microseconds < 0 or microseconds >= definitions.MICROSECONDS_PER_SECOND:
1404
+ raise ValueError("Invalid number of microseconds.")
1405
+
1406
+ fraction_of_second = (
1407
+ decimal.Decimal(microseconds) / definitions.MICROSECONDS_PER_SECOND
1408
+ )
1409
+
1410
+ super().__init__(
1411
+ fraction_of_second=fraction_of_second,
1412
+ is_delta=is_delta,
1413
+ precision=precision or definitions.PRECISION_1_MICROSECOND,
1414
+ time_elements_tuple=time_elements_tuple,
1415
+ time_zone_offset=time_zone_offset,
1416
+ )
1417
+
1418
+ @property
1419
+ def microseconds(self):
1420
+ """int: number of microseconds."""
1421
+ return int(self.fraction_of_second * definitions.MICROSECONDS_PER_SECOND)
1422
+
1423
+ def CopyFromStringTuple(self, time_elements_tuple):
1424
+ """Copies time elements from string-based time elements tuple.
1425
+
1426
+ Args:
1427
+ time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
1428
+ time elements, contains year, month, day of month, hours, minutes,
1429
+ seconds and microseconds.
1430
+
1431
+ Raises:
1432
+ ValueError: if the time elements tuple is invalid.
1433
+ """
1434
+ number_of_elements = len(time_elements_tuple)
1435
+ if len(time_elements_tuple) < 7:
1436
+ raise ValueError(
1437
+ (
1438
+ f"Invalid time elements tuple at least 7 elements required,"
1439
+ f"got: {number_of_elements:d}"
1440
+ )
1441
+ )
1442
+
1443
+ year, month, day_of_month, hours, minutes, seconds, microseconds = (
1444
+ time_elements_tuple
1445
+ )
1446
+ try:
1447
+ microseconds = int(microseconds, 10)
1448
+ except (TypeError, ValueError):
1449
+ raise ValueError(f"Invalid microsecond value: {microseconds!s}")
1450
+
1451
+ if microseconds < 0 or microseconds >= definitions.MICROSECONDS_PER_SECOND:
1452
+ raise ValueError("Invalid number of microseconds.")
1453
+
1454
+ fraction_of_second = (
1455
+ decimal.Decimal(microseconds) / definitions.MICROSECONDS_PER_SECOND
1456
+ )
1457
+ time_elements_tuple = (
1458
+ year,
1459
+ month,
1460
+ day_of_month,
1461
+ hours,
1462
+ minutes,
1463
+ seconds,
1464
+ str(fraction_of_second),
1465
+ )
1466
+ super().CopyFromStringTuple(time_elements_tuple)
1467
+
1468
+ def CopyToSerializableDict(self):
1469
+ """Copies the date time value to a serializable dictionary.
1470
+
1471
+ Returns:
1472
+ dict[str, object]: serializable dictionary.
1473
+ """
1474
+ serializable_dict = self._CreateSerializableDict()
1475
+
1476
+ serializable_dict["time_elements_tuple"] = (
1477
+ *self._time_elements_tuple,
1478
+ self.microseconds,
1479
+ )
1480
+
1481
+ if self._is_delta:
1482
+ serializable_dict["is_delta"] = True
1483
+
1484
+ return serializable_dict
1485
+
1486
+ def NewFromDeltaAndDate(self, year, month, day_of_month):
1487
+ """Creates a new time elements instance from a date time delta and a year.
1488
+
1489
+ Args:
1490
+ year (int): year.
1491
+ month (int): month, where 1 represents January and 0 if not set.
1492
+ day_of_month (int): day of month, where 1 represents the first day and 0
1493
+ if not set.
1494
+
1495
+ Returns:
1496
+ TimeElementsInMicroseconds: time elements or None if time elements are
1497
+ missing.
1498
+
1499
+ Raises:
1500
+ ValueError: if the instance is not a date time delta.
1501
+ """
1502
+ if not self._is_delta:
1503
+ raise ValueError("Not a date time delta.")
1504
+
1505
+ if self._time_elements_tuple is None:
1506
+ return None
1507
+
1508
+ delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
1509
+ self._time_elements_tuple
1510
+ )
1511
+ time_elements_tuple = (
1512
+ year + delta_year,
1513
+ month + delta_month,
1514
+ day_of_month + delta_day_of_month,
1515
+ hours,
1516
+ minutes,
1517
+ seconds,
1518
+ self.microseconds,
1519
+ )
1520
+ return TimeElementsInMicroseconds(
1521
+ precision=self._precision,
1522
+ time_elements_tuple=time_elements_tuple,
1523
+ time_zone_offset=self._time_zone_offset,
1524
+ )
1525
+
1526
+ def NewFromDeltaAndYear(self, year):
1527
+ """Creates a new time elements instance from a date time delta and a year.
1528
+
1529
+ Args:
1530
+ year (int): year.
1531
+
1532
+ Returns:
1533
+ TimeElementsInMicroseconds: time elements or None if time elements are
1534
+ missing.
1535
+
1536
+ Raises:
1537
+ ValueError: if the instance is not a date time delta.
1538
+ """
1539
+ return self.NewFromDeltaAndDate(year, 0, 0)
1540
+
1541
+
1542
+ class TimeElementsInNanoseconds(TimeElementsWithFractionOfSecond):
1543
+ """Time elements in nanoseconds.
1544
+
1545
+ Attributes:
1546
+ fraction_of_second (decimal.Decimal): fraction of second, which must be a
1547
+ value between 0.0 and 1.0.
1548
+ is_local_time (bool): True if the date and time value is in local time.
1549
+ precision (str): precision of the date of the date and time value, that
1550
+ represents 1 nanosecond (PRECISION_1_NANOSECOND).
1551
+ """
1552
+
1553
+ def __init__(
1554
+ self,
1555
+ is_delta=False,
1556
+ precision=None,
1557
+ time_elements_tuple=None,
1558
+ time_zone_offset=None,
1559
+ ):
1560
+ """Initializes time elements.
1561
+
1562
+ Args:
1563
+ is_delta (Optional[bool]): True if the date and time value is relative to
1564
+ another date and time value.
1565
+ precision (Optional[str]): precision of the date and time value, which
1566
+ should be one of the PRECISION_VALUES in definitions.
1567
+ time_elements_tuple (Optional[tuple[int, int, int, int, int, int, int]]):
1568
+ time elements, contains year, month, day of month, hours, minutes,
1569
+ seconds and nanoseconds.
1570
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
1571
+ from UTC or None if not set.
1572
+
1573
+ Raises:
1574
+ ValueError: if the time elements tuple is invalid.
1575
+ """
1576
+ fraction_of_second = None
1577
+ if time_elements_tuple:
1578
+ number_of_elements = len(time_elements_tuple)
1579
+ if number_of_elements < 7:
1580
+ raise ValueError(
1581
+ (
1582
+ f"Invalid time elements tuple at least 7 elements required,"
1583
+ f"got: {number_of_elements:d}"
1584
+ )
1585
+ )
1586
+
1587
+ nanoseconds = time_elements_tuple[6]
1588
+ time_elements_tuple = time_elements_tuple[:6]
1589
+
1590
+ if nanoseconds < 0 or nanoseconds >= definitions.NANOSECONDS_PER_SECOND:
1591
+ raise ValueError("Invalid number of nanoseconds.")
1592
+
1593
+ fraction_of_second = (
1594
+ decimal.Decimal(nanoseconds) / definitions.NANOSECONDS_PER_SECOND
1595
+ )
1596
+
1597
+ super().__init__(
1598
+ fraction_of_second=fraction_of_second,
1599
+ is_delta=is_delta,
1600
+ precision=precision or definitions.PRECISION_1_NANOSECOND,
1601
+ time_elements_tuple=time_elements_tuple,
1602
+ time_zone_offset=time_zone_offset,
1603
+ )
1604
+
1605
+ @property
1606
+ def nanoseconds(self):
1607
+ """int: number of nanoseconds."""
1608
+ return int(self.fraction_of_second * definitions.NANOSECONDS_PER_SECOND)
1609
+
1610
+ def CopyFromStringTuple(self, time_elements_tuple):
1611
+ """Copies time elements from string-based time elements tuple.
1612
+
1613
+ Args:
1614
+ time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
1615
+ time elements, contains year, month, day of month, hours, minutes,
1616
+ seconds and nanoseconds.
1617
+
1618
+ Raises:
1619
+ ValueError: if the time elements tuple is invalid.
1620
+ """
1621
+ number_of_elements = len(time_elements_tuple)
1622
+ if len(time_elements_tuple) < 7:
1623
+ raise ValueError(
1624
+ (
1625
+ f"Invalid time elements tuple at least 7 elements required,"
1626
+ f"got: {number_of_elements:d}"
1627
+ )
1628
+ )
1629
+
1630
+ year, month, day_of_month, hours, minutes, seconds, nanoseconds = (
1631
+ time_elements_tuple
1632
+ )
1633
+ try:
1634
+ nanoseconds = int(nanoseconds, 10)
1635
+ except (TypeError, ValueError):
1636
+ raise ValueError(f"Invalid nanosecond value: {nanoseconds!s}")
1637
+
1638
+ if nanoseconds < 0 or nanoseconds >= definitions.NANOSECONDS_PER_SECOND:
1639
+ raise ValueError("Invalid number of nanoseconds.")
1640
+
1641
+ fraction_of_second = (
1642
+ decimal.Decimal(nanoseconds) / definitions.NANOSECONDS_PER_SECOND
1643
+ )
1644
+ time_elements_tuple = (
1645
+ year,
1646
+ month,
1647
+ day_of_month,
1648
+ hours,
1649
+ minutes,
1650
+ seconds,
1651
+ str(fraction_of_second),
1652
+ )
1653
+ super().CopyFromStringTuple(time_elements_tuple)
1654
+
1655
+ def CopyToSerializableDict(self):
1656
+ """Copies the date time value to a serializable dictionary.
1657
+
1658
+ Returns:
1659
+ dict[str, object]: serializable dictionary.
1660
+ """
1661
+ serializable_dict = self._CreateSerializableDict()
1662
+
1663
+ serializable_dict["time_elements_tuple"] = (
1664
+ *self._time_elements_tuple,
1665
+ self.nanoseconds,
1666
+ )
1667
+
1668
+ if self._is_delta:
1669
+ serializable_dict["is_delta"] = True
1670
+
1671
+ return serializable_dict
1672
+
1673
+ def NewFromDeltaAndDate(self, year, month, day_of_month):
1674
+ """Creates a new time elements instance from a date time delta and a year.
1675
+
1676
+ Args:
1677
+ year (int): year.
1678
+ month (int): month, where 1 represents January and 0 if not set.
1679
+ day_of_month (int): day of month, where 1 represents the first day and 0
1680
+ if not set.
1681
+
1682
+ Returns:
1683
+ TimeElementsInNanoseconds: time elements or None if time elements are
1684
+ missing.
1685
+
1686
+ Raises:
1687
+ ValueError: if the instance is not a date time delta.
1688
+ """
1689
+ if not self._is_delta:
1690
+ raise ValueError("Not a date time delta.")
1691
+
1692
+ if self._time_elements_tuple is None:
1693
+ return None
1694
+
1695
+ delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
1696
+ self._time_elements_tuple
1697
+ )
1698
+ time_elements_tuple = (
1699
+ year + delta_year,
1700
+ month + delta_month,
1701
+ day_of_month + delta_day_of_month,
1702
+ hours,
1703
+ minutes,
1704
+ seconds,
1705
+ self.nanoseconds,
1706
+ )
1707
+ return TimeElementsInNanoseconds(
1708
+ precision=self._precision,
1709
+ time_elements_tuple=time_elements_tuple,
1710
+ time_zone_offset=self._time_zone_offset,
1711
+ )
1712
+
1713
+ def NewFromDeltaAndYear(self, year):
1714
+ """Creates a new time elements instance from a date time delta and a year.
1715
+
1716
+ Args:
1717
+ year (int): year.
1718
+
1719
+ Returns:
1720
+ TimeElementsInNanoseconds: time elements or None if time elements are
1721
+ missing.
1722
+
1723
+ Raises:
1724
+ ValueError: if the instance is not a date time delta.
1725
+ """
1726
+ return self.NewFromDeltaAndDate(year, 0, 0)
1727
+
1728
+
1729
+ factory.Factory.RegisterDateTimeValues(TimeElements)
1730
+ factory.Factory.RegisterDateTimeValues(TimeElementsInMilliseconds)
1731
+ factory.Factory.RegisterDateTimeValues(TimeElementsInMicroseconds)
1732
+ factory.Factory.RegisterDateTimeValues(TimeElementsInNanoseconds)