yta-video-frame-time 0.0.5__py3-none-any.whl → 0.0.16__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,1818 @@
1
+ """
2
+ TODO: This module is general so we should send it
3
+ to another library related to time intervals and
4
+ not specifically video frame times... Move it.
5
+ """
6
+ from yta_video_frame_time.decorators import parameter_to_time_interval, parameter_to_new_time_interval
7
+ from yta_video_frame_time.t_fraction import THandler
8
+ from yta_validation.parameter import ParameterValidator
9
+ from quicktions import Fraction
10
+ from typing import Union
11
+
12
+
13
+ Number = Union[int, float, Fraction]
14
+ """
15
+ Custom type to represent numbers.
16
+ """
17
+ TimeIntervalType = Union['TimeInterval', tuple[float, float]]
18
+ """
19
+ The type we accept as time interval, that could be a
20
+ tuple we transform into a time interval.
21
+ """
22
+
23
+ """
24
+ TODO: Maybe we can add the possibility of having an
25
+ `fps` value when initializing it to be able to force
26
+ the time interval values to be multiple of `1/fps`.
27
+ But this, if implemented, should be `TimeIntervalFPS`
28
+ or similar, and inheritance from this one but forcing
29
+ the values to be transformed according to that `1/fps`.
30
+ """
31
+ class TimeInterval:
32
+ """
33
+ Class to represent a time interval, which is a tuple
34
+ of time moments representing the time range
35
+ `[start, end)`.
36
+ """
37
+
38
+ @property
39
+ def duration(
40
+ self
41
+ ) -> float:
42
+ """
43
+ The `duration` of the time interval.
44
+ """
45
+ return self.end - self.start
46
+
47
+ @property
48
+ def copy(
49
+ self
50
+ ) -> 'TimeInterval':
51
+ """
52
+ A copy of this instance.
53
+ """
54
+ return TimeInterval(
55
+ start = self.start,
56
+ end = self.end
57
+ )
58
+
59
+ @property
60
+ def as_tuple(
61
+ self
62
+ ) -> tuple[float, float]:
63
+ """
64
+ The time interval but as a `(start, end)` tuple.
65
+ """
66
+ return (self.start, self.end)
67
+
68
+ @property
69
+ def cutter(
70
+ self
71
+ ) -> 'TimeIntervalCutter':
72
+ """
73
+ Shortcut to the static class `TimeIntervalCutter` that
74
+ is capable of cutting time intervals.
75
+ """
76
+ return TimeIntervalCutter
77
+
78
+ def __init__(
79
+ self,
80
+ start: Number,
81
+ end: Number,
82
+ ):
83
+ """
84
+ The `end` value must be greater than the `start` value.
85
+ """
86
+ if start > end:
87
+ raise Exception('The `start` value provided is greater than the `end` value provided.')
88
+
89
+ if start == end:
90
+ raise Exception('The `start` value provided is exactly the `end` value provided.')
91
+
92
+ self.start: float = start
93
+ """
94
+ The original `start` of the time segment.
95
+ """
96
+ self.end: float = end
97
+ """
98
+ The original `end` of the time segment.
99
+ """
100
+
101
+ # TODO: Is this that necessary (?)
102
+ def _validate_t(
103
+ self,
104
+ t: Number,
105
+ do_include_start: bool = False,
106
+ do_include_end: bool = False
107
+ ) -> None:
108
+ """
109
+ Validate that the provided `t` value is between the `start`
110
+ and the `end` parameters provided, including them or not
111
+ according to the boolean parameters provided.
112
+ """
113
+ ParameterValidator.validate_mandatory_number_between(
114
+ name = 't',
115
+ value = t,
116
+ lower_limit = self.start,
117
+ upper_limit = self.end,
118
+ do_include_lower_limit = do_include_start,
119
+ do_include_upper_limit = do_include_end
120
+ )
121
+
122
+ def cut(
123
+ self,
124
+ start: Number,
125
+ end: Number
126
+ ) -> tuple[Union['TimeInterval', None], Union['TimeInterval', None], Union['TimeInterval', None]]:
127
+ """
128
+ Cut a segment from the given `start` to the also provided
129
+ `end` time moments of this time interval instance.
130
+
131
+ This method will return a tuple of 3 elements including the
132
+ segments created by cutting this time interval in the order
133
+ they were generated, but also having the 4th element always
134
+ as the index of the one specifically requested by the user.
135
+ The tuple will include all the segments at the begining and
136
+ the rest will be None (unless the 4th one, which is the
137
+ index).
138
+
139
+ Examples below:
140
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
141
+ will generate `((2, 3), (3, 4), (4, 5), 1)`.
142
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
143
+ will generate `((2, 4), (4, 5), None, 0)`.
144
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
145
+ will generate `((2, 4), (4, 5), None, 1)`.
146
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
147
+ will generate `((2, 5), None, None, 0)`.
148
+
149
+ As you can see, the result could be the same in different
150
+ situations, but it's up to you (and the specific method in
151
+ which you are calling to this one) to choose the tuple you
152
+ want to return.
153
+ """
154
+ return self.cutter.from_to(
155
+ time_interval = self,
156
+ start = start,
157
+ end = end
158
+ )
159
+
160
+ def cutted(
161
+ self,
162
+ start: Number,
163
+ end: Number
164
+ ) -> 'TimeInterval':
165
+ """
166
+ Get this time interval instance but cutted from the `start`
167
+ to the `end` time moments provided.
168
+
169
+ (!) This method doesn't modify the original instance but
170
+ returns a new one.
171
+ """
172
+ tuples = self.cut(
173
+ start = start,
174
+ end = end
175
+ )
176
+
177
+ return tuples[tuples[3]]
178
+
179
+ def trim_start(
180
+ self,
181
+ t_variation: Number,
182
+ limit: Union[Number, None] = None
183
+ ) -> tuple['TimeInterval', 'TimeInterval']:
184
+ """
185
+ Get a tuple containing the 2 new `TimeInterval` instances
186
+ generated by trimming this one's start the amount of seconds
187
+ provided as the `t_variation` parameter. The first tuple is
188
+ the remaining, and the second one is the new time interval
189
+ requested by the user.
190
+
191
+ This method will raise an exception if the new `start` value
192
+ becomes a value over the time interval `end` value or the
193
+ `limit`, that must be greater than the `start` and lower
194
+ than the time interval `end` value.
195
+
196
+ The `t_variation` must be a positive value, the amount of
197
+ seconds to be trimmed.
198
+ """
199
+ return self.cutter.trim_start(
200
+ time_interval = self,
201
+ t_variation = t_variation,
202
+ limit = limit
203
+ )
204
+
205
+ def trimmed_start(
206
+ self,
207
+ t_variation: Number,
208
+ limit: Union[Number, None] = None
209
+ ) -> 'TimeInterval':
210
+ """
211
+ Get this time interval instance but trimmed from the `start`
212
+ the `t_variation` amount of seconds provided.
213
+
214
+ (!) This method doesn't modify the original instance but
215
+ returns a new one.
216
+ """
217
+ return self.trim_start(
218
+ t_variation = t_variation,
219
+ limit = limit
220
+ )[1]
221
+
222
+ def trim_end(
223
+ self,
224
+ t_variation: Number,
225
+ limit: Union[Number, None] = None
226
+ ) -> tuple['TimeInterval', 'TimeInterval']:
227
+ """
228
+ Get a tuple containing the 2 new `TimeInterval` instances
229
+ generated by trimming this one's end the amount of seconds
230
+ provided as the `t_variation` parameter. The first tuple is
231
+ the one requested by the user, and the second one is the
232
+ remaining.
233
+
234
+ This method will raise an exception if the new `end` value
235
+ becomes a value under the time interval `start` value or
236
+ the `limit`, that must be greater than the `start` and lower
237
+ than the time interval `end` value.
238
+
239
+ The `t_variation` must be a positive value, the amount of
240
+ seconds to be trimmed.
241
+ """
242
+ return self.cutter.trim_end(
243
+ time_interval = self,
244
+ t_variation = t_variation,
245
+ limit = limit
246
+ )
247
+
248
+ def trimmed_end(
249
+ self,
250
+ t_variation: Number,
251
+ limit: Union[Number, None] = None
252
+ ) -> 'TimeInterval':
253
+ """
254
+ Get this time interval instance but trimmed from the `end`
255
+ the `t_variation` amount of seconds provided.
256
+
257
+ (!) This method doesn't modify the original instance but
258
+ returns a new one.
259
+ """
260
+ return self.trim_end(
261
+ t_variation = t_variation,
262
+ limit = limit
263
+ )[0]
264
+
265
+ def split(
266
+ self,
267
+ t: Number
268
+ ) -> tuple['TimeInterval', 'TimeInterval']:
269
+ """
270
+ Split the time interval at the provided `t` time moment
271
+ and get the 2 new time intervals as a result (as a tuple).
272
+
273
+ This method will raise an exception if the `t` value
274
+ provided is a limit value (or above).
275
+
276
+ Examples below:
277
+ - A time interval of `[2, 5)` cut with `t=3` will generate
278
+ `((2, 3), (3, 5))`.
279
+ - A time interval of `[2, 5)` cut with `t=4` will generate
280
+ `((2, 4), (4, 5))`.
281
+ - A time interval of `[2, 5)` cut with `t>=5` will raise
282
+ exception.
283
+ - A time interval of `[2, 5)` cut with `t<=2` will raise
284
+ exception.
285
+ """
286
+ return self.cutter.split(
287
+ time_interval = self,
288
+ t = t
289
+ )
290
+
291
+ def splitted_left(
292
+ self,
293
+ t: Number
294
+ ) -> 'TimeInterval':
295
+ """
296
+ Split this time interval instance by the `t` time moment
297
+ provided and obtain the time interval from the left side,
298
+ which goes from this time interval `start` time moment to
299
+ the `t` provided.
300
+ """
301
+ return self.split(t)[0]
302
+
303
+ def splitted_right(
304
+ self,
305
+ t: Number
306
+ ) -> 'TimeInterval':
307
+ """
308
+ Split this time interval instance by the `t` time moment
309
+ provided and obtain the time interval from the right side,
310
+ which goes from the `t` provided to the time interval `end`
311
+ time moment.
312
+ """
313
+ return self.split(t)[1]
314
+
315
+ # Other methods below
316
+ def is_t_included(
317
+ self,
318
+ t: float,
319
+ do_include_end: bool = False
320
+ ) -> bool:
321
+ """
322
+ Check if the `t` time moment provided is included in
323
+ this time interval, including the `end` only if the
324
+ `do_include_end` parameter is set as `True`.
325
+ """
326
+ return TimeIntervalUtils.a_includes_t(
327
+ t = t,
328
+ time_interval_a = self,
329
+ do_include_end = do_include_end
330
+ )
331
+
332
+ def is_adjacent_to(
333
+ self,
334
+ time_interval: 'TimeInterval'
335
+ ) -> bool:
336
+ """
337
+ Check if the `time_interval` provided is adjacent
338
+ to this time interval, which means that the `end`
339
+ of one interval is also the `start` of the other
340
+ one.
341
+
342
+ (!) Giving the time intervals inverted will
343
+ provide the same result.
344
+
345
+ Example below:
346
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
347
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
348
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
349
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
350
+ """
351
+ return TimeIntervalUtils.a_is_adjacent_to_b(
352
+ time_interval_a = self,
353
+ time_interval_b = time_interval
354
+ )
355
+
356
+ def do_contains_a(
357
+ self,
358
+ time_interval: 'TimeInterval'
359
+ ) -> bool:
360
+ """
361
+ Check if this time interval includes the `time_interval`
362
+ provided or not, which means that the `time_interval`
363
+ provided is fully contained (included) in this one.
364
+ """
365
+ return TimeIntervalUtils.a_contains_b(
366
+ time_interval_a = self,
367
+ time_interval_b = time_interval
368
+ )
369
+
370
+ def is_contained_in(
371
+ self,
372
+ time_interval: 'TimeInterval'
373
+ ) -> bool:
374
+ """
375
+ Check if this time interval is fully contained in
376
+ the `time_interval` provided, which is a synonim
377
+ of being fully overlapped by that `time_interval`.
378
+ """
379
+ return TimeIntervalUtils.a_is_contained_in_b(
380
+ time_interval_a = self,
381
+ time_interval_b = time_interval
382
+ )
383
+
384
+ def do_intersects_with(
385
+ self,
386
+ time_interval: 'TimeInterval'
387
+ ) -> bool:
388
+ """
389
+ Check if this time interval intersects with the one
390
+ provided as `time_interval`, which means that they
391
+ have at least a part in common.
392
+ """
393
+ return TimeIntervalUtils.a_intersects_with_b(
394
+ time_interval_a = self,
395
+ time_interval_b = time_interval
396
+ )
397
+
398
+ def get_intersection_with_a(
399
+ self,
400
+ time_interval: 'TimeInterval'
401
+ ) -> Union['TimeInterval', None]:
402
+ """
403
+ Get the time interval that intersects this one and the
404
+ one provided as `time_interval`. The result can be `None`
405
+ if there is no intersection in between both.
406
+ """
407
+ return TimeIntervalUtils.get_intersection_of_a_and_b(
408
+ time_interval_a = self,
409
+ time_interval_b = time_interval
410
+ )
411
+
412
+ class TimeIntervalWithFps(TimeInterval):
413
+ """
414
+ Class to represent a time interval, which is a tuple
415
+ of time moments representing the time range
416
+ `[start, end)`, but with a `fps` value set to round
417
+ all the time moments to values multiple of `1/fps`.
418
+
419
+ This class is very useful when handling video time
420
+ moments and time ranges.
421
+ """
422
+
423
+ @property
424
+ def copy(
425
+ self
426
+ ) -> 'TimeInterval':
427
+ """
428
+ A copy of this instance.
429
+ """
430
+ return TimeIntervalWithFps(
431
+ start = self.start,
432
+ end = self.end,
433
+ fps = self.fps
434
+ )
435
+
436
+ def __init__(
437
+ self,
438
+ start: Number,
439
+ end: Number,
440
+ fps: Number
441
+ ):
442
+ """
443
+ (!) The `start` and `end` values will be truncated
444
+ according to the `fps` provided.
445
+
446
+ The `end` value must be greater than the `start` value.
447
+ """
448
+ ParameterValidator.validate_mandatory_positive_number('fps', fps, do_include_zero = False)
449
+
450
+ self.fps: float = fps
451
+ """
452
+ The `fps` for this time interval calculations.
453
+ """
454
+ self._t_handler = THandler(self.fps)
455
+ """
456
+ *For internal use only*
457
+
458
+ Shortcut to the `THandler` instance built with the `fps`
459
+ value provided when instantiating this instance.
460
+ """
461
+
462
+ super().__init__(
463
+ start = self._t_handler.t.truncated(start),
464
+ end = self._t_handler.t.truncated(end)
465
+ )
466
+
467
+ """
468
+ Apparently, the professional video editors use always
469
+ the `truncate` method to obtain the `start` of the
470
+ frame time interval always. So, why should I change it?
471
+ """
472
+ def _truncate(
473
+ self,
474
+ t: Number
475
+ ) -> float:
476
+ """
477
+ Get the truncated value of the `t` time moment provided,
478
+ which will be always the `start` of the time interval
479
+ delimited by the `t` and the `fps` of this instance.
480
+
481
+ Some examples below (with `fps=5`):
482
+ - `t=0.2` => `0.2` <=> `interval=[0.2, 0.4)`
483
+ - `t=0.37` => `0.2` <=> `interval=[0.2, 0.4)`
484
+ - `t=0.4` => `0.4` <=> `interval=[0.4, 0.6)`
485
+ - `t=0.87` => `0.8` <=> `interval=[0.8, 1.0)`
486
+ """
487
+ return self._t_handler.t.truncated(t)
488
+
489
+ def _round(
490
+ self,
491
+ t: Number
492
+ ) -> float:
493
+ """
494
+ Get the rounded value of the `t` time moment provided,
495
+ which will be the `start` or the `end` (depending on which
496
+ one is closer to the `t` value) of the time interval
497
+ delimited by the `t` and the `fps` of this instance.
498
+
499
+ Some examples below (with `fps=5`):
500
+ - `t=0.2` => `0.2` <=> `interval=[0.2, 0.4)`
501
+ - `t=0.29` => `0.2` <=> `interval=[0.2, 0.4)`
502
+ - `t=0.31` => `0.4` <=> `interval=[0.2, 0.4)`
503
+ - `t=0.37` => `0.4` <=> `interval=[0.2, 0.4)`
504
+ - `t=0.4` => `0.4` <=> `interval=[0.4, 0.6)`
505
+ - `t=0.87` => `0.8` <=> `interval=[0.8, 1.0)`
506
+ """
507
+ return self._t_handler.t.rounded(t)
508
+
509
+ def _round_up(
510
+ self,
511
+ t: Number
512
+ ) -> float:
513
+ """
514
+ Get the rounded up value of the `t` time moment provided,
515
+ which will be the `end` of the time interval (unless it
516
+ is exactly the `start` value of a time interval) delimited
517
+ by the `t` and the `fps` of this instance.
518
+
519
+ Some examples below (with `fps=5`):
520
+ - `t=0.2` => `0.2` <=> `interval=[0.2, 0.4)`
521
+ - `t=0.29` => `0.4` <=> `interval=[0.2, 0.4)`
522
+ - `t=0.31` => `0.4` <=> `interval=[0.2, 0.4)`
523
+ - `t=0.37` => `0.2` <=> `interval=[0.2, 0.4)`
524
+ - `t=0.4` => `0.4` <=> `interval=[0.4, 0.6)`
525
+ - `t=0.87` => `0.8` <=> `interval=[0.8, 1.0)`
526
+ """
527
+ return self._t_handler.t.rounded_up(t)
528
+
529
+ def cut(
530
+ self,
531
+ start: Number,
532
+ end: Number
533
+ ) -> tuple[Union['TimeInterval', None], Union['TimeInterval', None], Union['TimeInterval', None]]:
534
+ """
535
+ Cut a segment from the given `start` to the also provided
536
+ `end` time moments of this time interval instance.
537
+
538
+ (!) The `start` and `end` values will be truncated to fit the
539
+ `start` value of the time interval they belong to according
540
+ to the `fps` of this instance.
541
+
542
+ This method will return a tuple of 3 elements including the
543
+ segments created by cutting this time interval in the order
544
+ they were generated, but also having the 4th element always
545
+ as the index of the one specifically requested by the user.
546
+ The tuple will include all the segments at the begining and
547
+ the rest will be None (unless the 4th one, which is the
548
+ index).
549
+
550
+ Examples below:
551
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
552
+ will generate `((2, 3), (3, 4), (4, 5), 1)`.
553
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
554
+ will generate `((2, 4), (4, 5), None, 0)`.
555
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
556
+ will generate `((2, 4), (4, 5), None, 1)`.
557
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
558
+ will generate `((2, 5), None, None, 0)`.
559
+
560
+ As you can see, the result could be the same in different
561
+ situations, but it's up to you (and the specific method in
562
+ which you are calling to this one) to choose the tuple you
563
+ want to return.
564
+ """
565
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
566
+ return super().cut(
567
+ start = self._truncate(start),
568
+ end = self._truncate(end)
569
+ )
570
+
571
+ def cutted(
572
+ self,
573
+ start: Number,
574
+ end: Number
575
+ ) -> 'TimeInterval':
576
+ """
577
+ Get this time interval instance but cutted from the `start`
578
+ to the `end` time moments provided.
579
+
580
+ (!) The `start` and `end` values will be truncated to fit the
581
+ `start` value of the time interval they belong to according
582
+ to the `fps` of this instance.
583
+
584
+ (!) This method doesn't modify the original instance but
585
+ returns a new one.
586
+ """
587
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
588
+ return super().cutted(
589
+ start = self._truncate(start),
590
+ end = self._truncate(end)
591
+ )
592
+
593
+ def trim_start(
594
+ self,
595
+ t_variation: Number,
596
+ limit: Union[Number, None] = None
597
+ ) -> tuple['TimeInterval', 'TimeInterval']:
598
+ """
599
+ Get a tuple containing the 2 new `TimeInterval` instances
600
+ generated by trimming this one's start the amount of seconds
601
+ provided as the `t_variation` parameter. The first tuple is
602
+ the remaining, and the second one is the new time interval
603
+ requested by the user.
604
+
605
+ (!) The `t_variation` value provided will be transformed into
606
+ a multiple of `1/fps` of this instance, and truncated to fit
607
+ the `start` of the time interval the new segments will belong
608
+ to.
609
+
610
+ This method will raise an exception if the new `start` value
611
+ becomes a value over the time interval `end` value or the
612
+ `limit`, that must be greater than the `start` and lower
613
+ than the time interval `end` value.
614
+
615
+ The `t_variation` must be a positive value, the amount of
616
+ seconds to be trimmed.
617
+ """
618
+ return super().trim_start(
619
+ # TODO: Is this ok (?)
620
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
621
+ t_variation = self._truncate(t_variation),
622
+ limit = limit
623
+ )
624
+
625
+ def trimmed_start(
626
+ self,
627
+ t_variation: Number,
628
+ limit: Union[Number, None] = None
629
+ ) -> 'TimeInterval':
630
+ """
631
+ Get this time interval instance but trimmed from the `start`
632
+ the `t_variation` amount of seconds provided.
633
+
634
+ (!) The `t_variation` value provided will be transformed into
635
+ a multiple of `1/fps` of this instance, and truncated to fit
636
+ the `start` of the time interval the new segments will belong
637
+ to.
638
+
639
+ (!) This method doesn't modify the original instance but
640
+ returns a new one.
641
+ """
642
+ return super().trimmed_start(
643
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
644
+ t_variation = self._truncate(t_variation),
645
+ limit = limit
646
+ )
647
+
648
+ def trim_end(
649
+ self,
650
+ t_variation: Number,
651
+ limit: Union[Number, None] = None
652
+ ) -> tuple['TimeInterval', 'TimeInterval']:
653
+ """
654
+ Get a tuple containing the 2 new `TimeInterval` instances
655
+ generated by trimming this one's end the amount of seconds
656
+ provided as the `t_variation` parameter. The first tuple is
657
+ the one requested by the user, and the second one is the
658
+ remaining.
659
+
660
+ (!) The `t_variation` value provided will be transformed into
661
+ a multiple of `1/fps` of this instance, and truncated to fit
662
+ the `start` of the time interval the new segments will belong
663
+ to.
664
+
665
+ This method will raise an exception if the new `end` value
666
+ becomes a value under the time interval `start` value or
667
+ the `limit`, that must be greater than the `start` and lower
668
+ than the time interval `end` value.
669
+
670
+ The `t_variation` must be a positive value, the amount of
671
+ seconds to be trimmed.
672
+ """
673
+ return super().trim_end(
674
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
675
+ t_variation = self._truncate(t_variation),
676
+ limit = limit
677
+ )
678
+
679
+ def trimmed_end(
680
+ self,
681
+ t_variation: Number,
682
+ limit: Union[Number, None] = None
683
+ ) -> 'TimeInterval':
684
+ """
685
+ Get this time interval instance but trimmed from the `end`
686
+ the `t_variation` amount of seconds provided.
687
+
688
+ (!) The `t_variation` value provided will be transformed into
689
+ a multiple of `1/fps` of this instance, and truncated to fit
690
+ the `start` of the time interval the new segments will belong
691
+ to.
692
+
693
+ (!) This method doesn't modify the original instance but
694
+ returns a new one.
695
+ """
696
+ return super().trimmed_end(
697
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
698
+ t_variation = self._truncate(t_variation),
699
+ limit = limit
700
+ )
701
+
702
+ def split(
703
+ self,
704
+ t: Number
705
+ ) -> tuple['TimeInterval', 'TimeInterval']:
706
+ """
707
+ Split the time interval at the provided `t` time moment
708
+ and get the 2 new time intervals as a result (as a tuple).
709
+
710
+ (!) The `t` value provided will be transformed into a
711
+ multiple of `1/fps` of this instance, and truncated to fit
712
+ the `start` of the time interval the new segments will belong
713
+ to.
714
+
715
+ This method will raise an exception if the `t` value
716
+ provided is a limit value (or above).
717
+
718
+ Examples below:
719
+ - A time interval of `[2, 5)` cut with `t=3` will generate
720
+ `((2, 3), (3, 5))`.
721
+ - A time interval of `[2, 5)` cut with `t=4` will generate
722
+ `((2, 4), (4, 5))`.
723
+ - A time interval of `[2, 5)` cut with `t>=5` will raise
724
+ exception.
725
+ - A time interval of `[2, 5)` cut with `t<=2` will raise
726
+ exception.
727
+ """
728
+ return super().split(
729
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
730
+ t = self._truncate(t)
731
+ )
732
+
733
+ # The only thing that changes in 'splitted_left' and
734
+ # 'splitted_right' is the docummentation, but the rest
735
+ # is the same, so I don't overwrite it...
736
+
737
+
738
+ class NewTimeInterval:
739
+ """
740
+ A time interval that has a memory of the original values
741
+ but can be modified and enshorted during its life time.
742
+
743
+ Class to represent a time interval, which is a tuple
744
+ of time moments representing the time range
745
+ `[start, end)`.
746
+
747
+ TODO: This will replace the old two versions by making
748
+ the 'fps' parameter optional.
749
+ """
750
+
751
+ @property
752
+ def duration(
753
+ self
754
+ ) -> float:
755
+ """
756
+ The `duration` of the time interval.
757
+ """
758
+ return self.end - self.start
759
+
760
+ @property
761
+ def copy(
762
+ self
763
+ ) -> 'NewTimeInterval':
764
+ """
765
+ A copy of this instance.
766
+ """
767
+ time_interval = NewTimeInterval(
768
+ start = self.start,
769
+ end = self.end,
770
+ fps = self.fps
771
+ )
772
+
773
+ time_interval.start = self.start
774
+ time_interval.end = self.end
775
+
776
+ return time_interval
777
+
778
+ @property
779
+ def as_tuple(
780
+ self
781
+ ) -> tuple[float, float]:
782
+ """
783
+ The time interval but as a `(start, end)` tuple.
784
+ """
785
+ return (self.start, self.end)
786
+
787
+ @property
788
+ def cutter(
789
+ self
790
+ ) -> 'TimeIntervalCutter':
791
+ """
792
+ Shortcut to the static class `TimeIntervalCutter` that
793
+ is capable of cutting time intervals.
794
+ """
795
+ return TimeIntervalCutter
796
+
797
+ def __init__(
798
+ self,
799
+ start: Number,
800
+ end: Number,
801
+ fps: Union[Number, None] = None
802
+ ):
803
+ """
804
+ The `end` value must be greater than the `start` value.
805
+
806
+ (!) If `fps` is provided, the `start` and `end` values
807
+ will be truncated according to the `fps` provided.
808
+ """
809
+ ParameterValidator.validate_positive_number('fps', fps, do_include_zero = False)
810
+
811
+ self.fps: Union[Number, None] = fps
812
+ """
813
+ The `fps` for this time interval calculations, if set.
814
+ """
815
+ self._t_handler: Union[THandler, None] = (
816
+ THandler(self.fps)
817
+ if self.fps is not None else
818
+ None
819
+ )
820
+ """
821
+ *For internal use only*
822
+
823
+ Shortcut to the `THandler` instance built with the `fps`
824
+ value provided when instantiating this instance, that
825
+ can be `None` if no `fps` are provided.
826
+ """
827
+
828
+ if start > end:
829
+ raise Exception('The `start` value provided is greater than the `end` value provided.')
830
+
831
+ if start == end:
832
+ raise Exception('The `start` value provided is exactly the `end` value provided.')
833
+
834
+ self.original_start: float = (
835
+ self._t_handler.t.truncated(start)
836
+ if self.fps is not None else
837
+ start
838
+ )
839
+ """
840
+ The original `start` of the time interval.
841
+ """
842
+ self.original_end: float = (
843
+ self._t_handler.t.truncated(end)
844
+ if self.fps is not None else
845
+ end
846
+ )
847
+ """
848
+ The original `end` of the time interval.
849
+ """
850
+ self.start: float = self.original_start
851
+ """
852
+ The current `start` value of this time interval.
853
+ """
854
+ self.end: float = self.original_end
855
+ """
856
+ The current `end` value of this time interval.
857
+ """
858
+
859
+ # TODO: Is this that necessary (?)
860
+ def _validate_t(
861
+ self,
862
+ t: Number,
863
+ do_include_start: bool = False,
864
+ do_include_end: bool = False
865
+ ) -> None:
866
+ """
867
+ Validate that the provided `t` value is between the `start`
868
+ and the `end` parameters provided, including them or not
869
+ according to the boolean parameters provided.
870
+ """
871
+ # TODO: Maybe we should validate also according to the
872
+ # original values
873
+ ParameterValidator.validate_mandatory_number_between(
874
+ name = 't',
875
+ value = t,
876
+ lower_limit = self.start,
877
+ upper_limit = self.end,
878
+ do_include_lower_limit = do_include_start,
879
+ do_include_upper_limit = do_include_end
880
+ )
881
+
882
+ """
883
+ Apparently, the professional video editors use always
884
+ the `truncate` method to obtain the `start` of the
885
+ frame time interval always. So, why should I change it?
886
+ """
887
+ def _truncate(
888
+ self,
889
+ t: Number
890
+ ) -> float:
891
+ """
892
+ Get the truncated value of the `t` time moment provided,
893
+ which will be always the `start` of the time interval
894
+ delimited by the `t` and the `fps` of this instance.
895
+
896
+ (!) The value will be truncated only if the `fps`
897
+ attribute is set.
898
+
899
+ Some examples below (with `fps=5`):
900
+ - `t=0.2` => `0.2` <=> `interval=[0.2, 0.4)`
901
+ - `t=0.37` => `0.2` <=> `interval=[0.2, 0.4)`
902
+ - `t=0.4` => `0.4` <=> `interval=[0.4, 0.6)`
903
+ - `t=0.87` => `0.8` <=> `interval=[0.8, 1.0)`
904
+ """
905
+ return (
906
+ self._t_handler.t.truncated(t)
907
+ if self.fps is not None else
908
+ t
909
+ )
910
+
911
+ def _round(
912
+ self,
913
+ t: Number
914
+ ) -> float:
915
+ """
916
+ Get the rounded value of the `t` time moment provided,
917
+ which will be the `start` or the `end` (depending on which
918
+ one is closer to the `t` value) of the time interval
919
+ delimited by the `t` and the `fps` of this instance.
920
+
921
+ (!) The value will be rounded only if the `fps`
922
+ attribute is set.
923
+
924
+ Some examples below (with `fps=5`):
925
+ - `t=0.2` => `0.2` <=> `interval=[0.2, 0.4)`
926
+ - `t=0.29` => `0.2` <=> `interval=[0.2, 0.4)`
927
+ - `t=0.31` => `0.4` <=> `interval=[0.2, 0.4)`
928
+ - `t=0.37` => `0.4` <=> `interval=[0.2, 0.4)`
929
+ - `t=0.4` => `0.4` <=> `interval=[0.4, 0.6)`
930
+ - `t=0.87` => `0.8` <=> `interval=[0.8, 1.0)`
931
+ """
932
+ return (
933
+ self._t_handler.t.rounded(t)
934
+ if self.fps is not None else
935
+ t
936
+ )
937
+
938
+ def _round_up(
939
+ self,
940
+ t: Number
941
+ ) -> float:
942
+ """
943
+ Get the rounded up value of the `t` time moment provided,
944
+ which will be the `end` of the time interval (unless it
945
+ is exactly the `start` value of a time interval) delimited
946
+ by the `t` and the `fps` of this instance.
947
+
948
+ (!) The value will be rounded up only if the `fps`
949
+ attribute is set.
950
+
951
+ Some examples below (with `fps=5`):
952
+ - `t=0.2` => `0.2` <=> `interval=[0.2, 0.4)`
953
+ - `t=0.29` => `0.4` <=> `interval=[0.2, 0.4)`
954
+ - `t=0.31` => `0.4` <=> `interval=[0.2, 0.4)`
955
+ - `t=0.37` => `0.2` <=> `interval=[0.2, 0.4)`
956
+ - `t=0.4` => `0.4` <=> `interval=[0.4, 0.6)`
957
+ - `t=0.87` => `0.8` <=> `interval=[0.8, 1.0)`
958
+ """
959
+ return (
960
+ self._t_handler.t.rounded_up(t)
961
+ if self.fps is not None else
962
+ t
963
+ )
964
+
965
+ def get_cuts(
966
+ self,
967
+ start: Number,
968
+ end: Number
969
+ ) -> tuple[Union['NewTimeInterval', None], Union['NewTimeInterval', None], Union['NewTimeInterval', None]]:
970
+ """
971
+ Cut a segment from the given `start` to the also provided
972
+ `end` time moments of this time interval instance and get
973
+ all the cuts.
974
+
975
+ This method will return a tuple of 3 elements including the
976
+ segments created by cutting this time interval in the order
977
+ they were generated, but also having the 4th element always
978
+ as the index of the one specifically requested by the user.
979
+ The tuple will include all the segments at the begining and
980
+ the rest will be None (unless the 4th one, which is the
981
+ index).
982
+
983
+ Examples below:
984
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
985
+ will generate `((2, 3), (3, 4), (4, 5), 1)`.
986
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
987
+ will generate `((2, 4), (4, 5), None, 0)`.
988
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
989
+ will generate `((2, 4), (4, 5), None, 1)`.
990
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
991
+ will generate `((2, 5), None, None, 0)`.
992
+
993
+ As you can see, the result could be the same in different
994
+ situations, but it's up to you (and the specific method in
995
+ which you are calling to this one) to choose the tuple you
996
+ want to return.
997
+ """
998
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
999
+ return self.cutter.from_to(
1000
+ time_interval = self,
1001
+ start = self._truncate(start),
1002
+ end = self._truncate(end)
1003
+ )
1004
+
1005
+ def get_cut(
1006
+ self,
1007
+ start: Number,
1008
+ end: Number
1009
+ ) -> 'NewTimeInterval':
1010
+ """
1011
+ Get this time interval instance but cutted from the `start`
1012
+ to the `end` time moments provided.
1013
+
1014
+ (!) This method doesn't modify the original instance but
1015
+ returns a new one.
1016
+ """
1017
+ tuples = self.get_cuts(
1018
+ start = start,
1019
+ end = end
1020
+ )
1021
+
1022
+ return tuples[tuples[3]]
1023
+
1024
+ def cut(
1025
+ self,
1026
+ start: Number,
1027
+ end: Number
1028
+ ) -> 'NewTimeInterval':
1029
+ """
1030
+ (!) This method will modify this instance.
1031
+
1032
+ Transform this time interval into a new one delimited by
1033
+ the `start` and `end` time moments provided.
1034
+
1035
+ This method returns this same instance but modified.
1036
+ """
1037
+ cut = self.get_cut(
1038
+ start = start,
1039
+ end = end
1040
+ )
1041
+
1042
+ self.start = cut.start
1043
+ self.end = cut.end
1044
+
1045
+ return self
1046
+
1047
+ # TODO: Rename, please
1048
+ def get_trim_starts(
1049
+ self,
1050
+ t_variation: Number,
1051
+ limit: Union[Number, None] = None
1052
+ ) -> tuple['NewTimeInterval', 'NewTimeInterval']:
1053
+ """
1054
+ Get a tuple containing the 2 new `TimeInterval` instances
1055
+ generated by trimming this one's start the amount of seconds
1056
+ provided as the `t_variation` parameter. The first tuple is
1057
+ the remaining, and the second one is the new time interval
1058
+ requested by the user.
1059
+
1060
+ (!) The `t_variation` value provided will be transformed into
1061
+ a multiple of `1/fps` of this instance, and truncated to fit
1062
+ the `start` of the time interval the new segments will belong
1063
+ to, if the `fps` is set.
1064
+
1065
+ This method will raise an exception if the new `start` value
1066
+ becomes a value over the time interval `end` value or the
1067
+ `limit`, that must be greater than the `start` and lower
1068
+ than the time interval `end` value.
1069
+
1070
+ The `t_variation` must be a positive value, the amount of
1071
+ seconds to be trimmed.
1072
+ """
1073
+ return self.cutter.trim_start(
1074
+ time_interval = self,
1075
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
1076
+ t_variation = self._truncate(t_variation),
1077
+ limit = limit
1078
+ )
1079
+
1080
+ # TODO: Rename, please
1081
+ def get_trim_start(
1082
+ self,
1083
+ t_variation: Number,
1084
+ limit: Union[Number, None] = None
1085
+ ) -> 'NewTimeInterval':
1086
+ """
1087
+ Get this time interval instance but trimmed from the `start`
1088
+ the `t_variation` amount of seconds provided.
1089
+
1090
+ (!) The `t_variation` value provided will be transformed into
1091
+ a multiple of `1/fps` of this instance, and truncated to fit
1092
+ the `start` of the time interval the new segments will belong
1093
+ to, if the `fps` is set.
1094
+
1095
+ (!) This method doesn't modify the original instance but
1096
+ returns a new one.
1097
+ """
1098
+ return self.get_trim_starts(
1099
+ t_variation = t_variation,
1100
+ limit = limit
1101
+ )[1]
1102
+
1103
+ def trim_start(
1104
+ self,
1105
+ t_variation: Number,
1106
+ limit: Union[Number, None] = None
1107
+ ) -> 'NewTimeInterval':
1108
+ """
1109
+ (!) This method will modify this instance.
1110
+
1111
+ Transform this time interval into a new one in which
1112
+ the `start` has been trimmed the `t_variation` provided
1113
+ if the result respected the also given `limit`.
1114
+
1115
+ This method returns this same instance but modified.
1116
+ """
1117
+ cut = self.get_trim_start(
1118
+ t_variation = t_variation,
1119
+ limit = limit
1120
+ )
1121
+
1122
+ self.start = cut.start
1123
+ self.end = cut.end
1124
+
1125
+ return self
1126
+
1127
+ def get_trim_ends(
1128
+ self,
1129
+ t_variation: Number,
1130
+ limit: Union[Number, None] = None
1131
+ ) -> tuple['NewTimeInterval', 'NewTimeInterval']:
1132
+ """
1133
+ Get a tuple containing the 2 new `TimeInterval` instances
1134
+ generated by trimming this one's end the amount of seconds
1135
+ provided as the `t_variation` parameter. The first tuple is
1136
+ the one requested by the user, and the second one is the
1137
+ remaining.
1138
+
1139
+ (!) The `t_variation` value provided will be transformed into
1140
+ a multiple of `1/fps` of this instance, and truncated to fit
1141
+ the `start` of the time interval the new segments will belong
1142
+ to, if the `fps` is set.
1143
+
1144
+ This method will raise an exception if the new `end` value
1145
+ becomes a value under the time interval `start` value or
1146
+ the `limit`, that must be greater than the `start` and lower
1147
+ than the time interval `end` value.
1148
+
1149
+ The `t_variation` must be a positive value, the amount of
1150
+ seconds to be trimmed.
1151
+ """
1152
+ return self.cutter.trim_end(
1153
+ time_interval = self,
1154
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
1155
+ t_variation = self._truncate(t_variation),
1156
+ limit = limit
1157
+ )
1158
+
1159
+ # TODO: Rename, please
1160
+ def get_trim_end(
1161
+ self,
1162
+ t_variation: Number,
1163
+ limit: Union[Number, None] = None
1164
+ ) -> 'NewTimeInterval':
1165
+ """
1166
+ Get this time interval instance but trimmed from the `end`
1167
+ the `t_variation` amount of seconds provided.
1168
+
1169
+ (!) The `t_variation` value provided will be transformed into
1170
+ a multiple of `1/fps` of this instance, and truncated to fit
1171
+ the `end` of the time interval the new segments will belong
1172
+ to, if the `fps` is set.
1173
+
1174
+ (!) This method doesn't modify the original instance but
1175
+ returns a new one.
1176
+ """
1177
+ return self.get_trim_ends(
1178
+ t_variation = t_variation,
1179
+ limit = limit
1180
+ )[0]
1181
+
1182
+ def trim_end(
1183
+ self,
1184
+ t_variation: Number,
1185
+ limit: Union[Number, None] = None
1186
+ ) -> 'NewTimeInterval':
1187
+ """
1188
+ (!) This method will modify this instance.
1189
+
1190
+ Transform this time interval into a new one in which
1191
+ the `end` has been trimmed the `t_variation` provided
1192
+ if the result respected the also given `limit`.
1193
+
1194
+ This method returns this same instance but modified.
1195
+ """
1196
+ cut = self.get_trim_end(
1197
+ t_variation = t_variation,
1198
+ limit = limit
1199
+ )
1200
+
1201
+ self.start = cut.start
1202
+ self.end = cut.end
1203
+
1204
+ return self
1205
+
1206
+ def get_splits(
1207
+ self,
1208
+ t: Number
1209
+ ) -> tuple['NewTimeInterval', 'NewTimeInterval']:
1210
+ """
1211
+ Split the time interval at the provided `t` time moment
1212
+ and get the 2 new time intervals as a result (as a tuple).
1213
+
1214
+ (!) The `t` value provided will be transformed into a
1215
+ multiple of `1/fps` of this instance, and truncated to fit
1216
+ the `start` of the time interval the new segments will belong
1217
+ to if the `fps` value is set.
1218
+
1219
+ This method will raise an exception if the `t` value
1220
+ provided is a limit value (or above).
1221
+
1222
+ Examples below:
1223
+ - A time interval of `[2, 5)` cut with `t=3` will generate
1224
+ `((2, 3), (3, 5))`.
1225
+ - A time interval of `[2, 5)` cut with `t=4` will generate
1226
+ `((2, 4), (4, 5))`.
1227
+ - A time interval of `[2, 5)` cut with `t>=5` will raise
1228
+ exception.
1229
+ - A time interval of `[2, 5)` cut with `t<=2` will raise
1230
+ exception.
1231
+ """
1232
+ return self.cutter.split(
1233
+ time_interval = self,
1234
+ # TODO: maybe a 'decorator' to do this '_truncate' (?)
1235
+ t = self._truncate(t)
1236
+ )
1237
+
1238
+ def split(
1239
+ self,
1240
+ t: Number
1241
+ ) -> tuple['NewTimeInterval', 'NewTimeInterval']:
1242
+ """
1243
+ Split the time interval at the provided `t` time moment
1244
+ and get the 2 new time intervals as a result (as a tuple),
1245
+ that will be copies of this instance (with their original
1246
+ `start` and `end` values) but the new ones according to
1247
+ the split result.
1248
+
1249
+ (!) The `t` value provided will be transformed into a
1250
+ multiple of `1/fps` of this instance, and truncated to fit
1251
+ the `start` of the time interval the new segments will belong
1252
+ to if the `fps` value is set.
1253
+
1254
+ This method will raise an exception if the `t` value
1255
+ provided is a limit value (or above).
1256
+ """
1257
+ splits = self.get_splits(t)
1258
+
1259
+ split_left = self.copy
1260
+ split_left.start = splits[0].start
1261
+ split_left.end = splits[0].end
1262
+
1263
+ split_right = self.copy
1264
+ split_right.start = splits[1].start
1265
+ split_right.end = splits[1].end
1266
+
1267
+ return (
1268
+ split_left, split_right
1269
+ )
1270
+
1271
+ # TODO: Implement 'extend' methods using the 'original'
1272
+ # values
1273
+
1274
+ class TimeIntervalUtils:
1275
+ """
1276
+ Static class to wrap the utils related to time intervals.
1277
+ """
1278
+
1279
+ @staticmethod
1280
+ def a_includes_t(
1281
+ t: float,
1282
+ time_interval_a: 'TimeInterval',
1283
+ do_include_end: bool = False
1284
+ ) -> bool:
1285
+ """
1286
+ Check if the `t` time moment provided is included in
1287
+ the `time_interval_a` given. The `time_interval_a.end`
1288
+ is excluded unless the `do_include_end` parameter is
1289
+ set as `True`.
1290
+
1291
+ A time interval is `[start, end)`, thats why the end is
1292
+ excluded by default.
1293
+ """
1294
+ return (
1295
+ time_interval_a.start <= t <= time_interval_a.end
1296
+ if do_include_end else
1297
+ time_interval_a.start <= t < time_interval_a.end
1298
+ )
1299
+
1300
+ @staticmethod
1301
+ def a_is_adjacent_to_b(
1302
+ time_interval_a: 'TimeInterval',
1303
+ time_interval_b: 'TimeInterval',
1304
+ ) -> bool:
1305
+ """
1306
+ Check if the `time_interval_a` provided and the
1307
+ also given `time_interval_b` are adjacent, which
1308
+ means that the `end` of one interval is also the
1309
+ `start` of the other one.
1310
+
1311
+ (!) Giving the time intervals inverted will
1312
+ provide the same result.
1313
+
1314
+ Examples below:
1315
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
1316
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
1317
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
1318
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
1319
+ """
1320
+ return (
1321
+ TimeIntervalUtils.a_is_inmediately_before_b(time_interval_a, time_interval_b) or
1322
+ TimeIntervalUtils.a_is_inmediately_after_b(time_interval_a, time_interval_b)
1323
+ )
1324
+
1325
+ @staticmethod
1326
+ def a_is_inmediately_before_b(
1327
+ time_interval_a: 'TimeInterval',
1328
+ time_interval_b: 'TimeInterval',
1329
+ ) -> bool:
1330
+ """
1331
+ Check if the `time_interval_a` provided is inmediately
1332
+ before the also given `time_interval_b`, which means
1333
+ that the `end` of the first one is also the `start` of
1334
+ the second one.
1335
+
1336
+ Examples below:
1337
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
1338
+ - `a=[5, 7)` and `b=[2, 5)` => `False`
1339
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
1340
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
1341
+ """
1342
+ return time_interval_a.end == time_interval_b.start
1343
+
1344
+ @staticmethod
1345
+ def a_is_inmediately_after_b(
1346
+ time_interval_a: 'TimeInterval',
1347
+ time_interval_b: 'TimeInterval',
1348
+ ) -> bool:
1349
+ """
1350
+ Check if the `time_interval_a` provided is inmediately
1351
+ after the also given `time_interval_b`, which means
1352
+ that the `start` of the first one is also the `end` of
1353
+ the second one.
1354
+
1355
+ Examples below:
1356
+ - `a=[2, 5)` and `b=[5, 7)` => `False`
1357
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
1358
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
1359
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
1360
+ """
1361
+ return time_interval_a.start == time_interval_b.end
1362
+
1363
+ @staticmethod
1364
+ def a_contains_b(
1365
+ time_interval_a: 'TimeInterval',
1366
+ time_interval_b: 'TimeInterval'
1367
+ ) -> bool:
1368
+ """
1369
+ Check if the `time_interval_a` time interval provided
1370
+ includes the `time_interval_b` or not, which means that
1371
+ the `time_interval_b` is fully contained in the first
1372
+ one.
1373
+
1374
+ Examples below:
1375
+ - `a=[2, 5)` and `b=[3, 4)` => `True`
1376
+ - `a=[2, 5)` and `b=[2, 4)` => `True`
1377
+ - `a=[2, 5)` and `b=[3, 6)` => `False`
1378
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
1379
+ """
1380
+ return (
1381
+ time_interval_a.start <= time_interval_b.start and
1382
+ time_interval_a.end >= time_interval_b.end
1383
+ )
1384
+
1385
+ @staticmethod
1386
+ def a_is_contained_in_b(
1387
+ time_interval_a: 'TimeInterval',
1388
+ time_interval_b: 'TimeInterval',
1389
+ ) -> bool:
1390
+ """
1391
+ Check if the `time_interval_a` provided is fully
1392
+ contained into the also provided `time_interval_b`.
1393
+
1394
+ Examples below:
1395
+ - `a=[2, 5)` and `b=[1, 6)` => `True`
1396
+ - `a=[2, 5)` and `b=[0, 9)` => `True`
1397
+ - `a=[2, 5)` and `b=[2, 4)` => `False`
1398
+ - `a=[2, 5)` and `b=[4, 8)` => `False`
1399
+ - `a=[2, 5)` and `b=[7, 8)` => `False`
1400
+ """
1401
+ return TimeIntervalUtils.a_contains_b(
1402
+ time_interval_a = time_interval_b,
1403
+ time_interval_b = time_interval_a
1404
+ )
1405
+
1406
+ @staticmethod
1407
+ def a_intersects_with_b(
1408
+ time_interval_a: 'TimeInterval',
1409
+ time_interval_b: 'TimeInterval',
1410
+ ) -> bool:
1411
+ """
1412
+ Check if the `time_interval_a` and the `time_interval_b`
1413
+ provided has at least a part in common.
1414
+
1415
+ Examples below:
1416
+ - `a=[2, 5)` and `b=[4, 6)` => `True`
1417
+ - `a=[2, 5)` and `b=[1, 3)` => `True`
1418
+ - `a=[2, 5)` and `b=[5, 6)` => `False`
1419
+ - `a=[2, 5)` and `b=[7, 8)` => `False`
1420
+ - `a=[2, 5)` and `b=[1, 2)` => `False`
1421
+ """
1422
+ return (
1423
+ time_interval_b.start < time_interval_a.end and
1424
+ time_interval_a.start < time_interval_b.end
1425
+ )
1426
+
1427
+ @staticmethod
1428
+ def get_intersection_of_a_and_b(
1429
+ time_interval_a: 'TimeInterval',
1430
+ time_interval_b: 'TimeInterval'
1431
+ ) -> Union['TimeInterval', None]:
1432
+ """
1433
+ Get the time interval that intersects the two time
1434
+ intervals provided, that can be `None` if there is no
1435
+ intersection in between both.
1436
+ """
1437
+ return (
1438
+ None
1439
+ if not TimeIntervalUtils.a_intersects_with_b(
1440
+ time_interval_a = time_interval_a,
1441
+ time_interval_b = time_interval_b
1442
+ ) else
1443
+ TimeInterval(
1444
+ start = max(time_interval_a.start, time_interval_b.start),
1445
+ end = min(time_interval_a.end, time_interval_b.end)
1446
+ )
1447
+ )
1448
+
1449
+ class TimeIntervalCutter:
1450
+ """
1451
+ Class to wrap the functionality related to cutting
1452
+ time intervals.
1453
+ """
1454
+
1455
+ """
1456
+ TODO: Methods I have to implement:
1457
+ trim_end → recorta la parte final
1458
+ trim_start → recorta la parte inicial
1459
+ cut_segment → elimina un tramo [a, b)
1460
+ split_at → divide en dos en un punto dado
1461
+ """
1462
+
1463
+ @staticmethod
1464
+ # @parameter_to_time_interval('time_interval')
1465
+ @parameter_to_new_time_interval('time_interval')
1466
+ def trim_end_to(
1467
+ time_interval: TimeIntervalType,
1468
+ t: Number
1469
+ ) -> tuple['TimeInterval', 'TimeInterval']:
1470
+ """
1471
+ Get a tuple containing the 2 new `TimeInterval` instances
1472
+ generated by trimming the `time_interval` end to the `t`
1473
+ time moment provided. The first tuple is the requested by
1474
+ the user, and the second one is the remaining.
1475
+
1476
+ The `t` time moment provided must be a value between the
1477
+ `start` and `end` of the `time_interval` provided.
1478
+ """
1479
+ time_interval._validate_t(t)
1480
+
1481
+ return (
1482
+ TimeInterval(
1483
+ start = time_interval.start,
1484
+ end = t
1485
+ ),
1486
+ TimeInterval(
1487
+ start = t,
1488
+ end = time_interval.end
1489
+ )
1490
+ )
1491
+
1492
+ @staticmethod
1493
+ # @parameter_to_time_interval('time_interval')
1494
+ @parameter_to_new_time_interval('time_interval')
1495
+ def trim_end(
1496
+ time_interval: TimeIntervalType,
1497
+ t_variation: Number,
1498
+ limit: Union[Number, None] = None,
1499
+ ) -> tuple['TimeInterval', 'TimeInterval']:
1500
+ """
1501
+ Get a tuple containing the 2 new `TimeInterval` instances
1502
+ generated by trimming the `time_interval` end the amount
1503
+ of seconds provided as the `t_variation` parameter. The
1504
+ first tuple is the requested by the user, and the second one
1505
+ is the remaining.
1506
+
1507
+ This method will raise an exception if the new `end` value
1508
+ becomes a value under the time interval `start` value or
1509
+ the `limit`, that must be greater than the `start` and lower
1510
+ than the time interval `end` value.
1511
+
1512
+ The `t_variation` must be a positive value, the amount of
1513
+ seconds to be trimmed.
1514
+ """
1515
+ ParameterValidator.validate_mandatory_positive_number('t_variation', t_variation, do_include_zero = False)
1516
+ ParameterValidator.validate_number_between('limit', limit, time_interval.start, time_interval.end, False, False)
1517
+
1518
+ new_end = time_interval.end - t_variation
1519
+ limit = (
1520
+ time_interval.start
1521
+ if limit is None else
1522
+ limit
1523
+ )
1524
+
1525
+ if new_end <= time_interval.start:
1526
+ raise Exception('The "t_variation" value provided makes the new end value be lower than the "start" value.')
1527
+
1528
+ if new_end <= limit:
1529
+ raise Exception('The "t_variation" value provided makes the new end value be lower than the "limit" value provided.')
1530
+
1531
+ return (
1532
+ TimeInterval(
1533
+ start = time_interval.start,
1534
+ end = new_end
1535
+ ),
1536
+ TimeInterval(
1537
+ start = new_end,
1538
+ end = time_interval.end
1539
+ )
1540
+ )
1541
+
1542
+ @staticmethod
1543
+ # @parameter_to_time_interval('time_interval')
1544
+ @parameter_to_new_time_interval('time_interval')
1545
+ def trim_start_to(
1546
+ time_interval: TimeIntervalType,
1547
+ t: Number
1548
+ ) -> tuple['TimeInterval', 'TimeInterval']:
1549
+ """
1550
+ Get a tuple containing the 2 new `TimeInterval` instances
1551
+ generated by trimming the `time_interval` start to the `t`
1552
+ time moment provided. The first tuple is the remaining, and
1553
+ the second one is the requested by the user.
1554
+
1555
+ The `t` time moment provided must be a value between the
1556
+ `start` and `end` of the `time_interval` provided.
1557
+ """
1558
+ time_interval._validate_t(t)
1559
+
1560
+ return (
1561
+ TimeInterval(
1562
+ start = time_interval.start,
1563
+ end = t
1564
+ ),
1565
+ TimeInterval(
1566
+ start = t,
1567
+ end = time_interval.end
1568
+ )
1569
+ )
1570
+
1571
+ @staticmethod
1572
+ # @parameter_to_time_interval('time_interval')
1573
+ @parameter_to_new_time_interval('time_interval')
1574
+ def trim_start(
1575
+ time_interval: TimeIntervalType,
1576
+ t_variation: Number,
1577
+ limit: Union[Number, None] = None,
1578
+ ) -> tuple['TimeInterval', 'TimeInterval']:
1579
+ """
1580
+ Get a tuple containing the 2 new `TimeInterval` instances
1581
+ generated by trimming the `time_interval` start the amount
1582
+ of seconds provided as the `t_variation` parameter. The
1583
+ first tuple is the remaining, and the second one is the
1584
+ new time interval requested by the user.
1585
+
1586
+ This method will raise an exception if the new `end` value
1587
+ becomes a value under the time interval `start` value or
1588
+ the `limit`, that must be greater than the `start` and lower
1589
+ than the time interval `end` value.
1590
+
1591
+ The `t_variation` must be a positive value, the amount of
1592
+ seconds to be trimmed.
1593
+ """
1594
+ ParameterValidator.validate_mandatory_positive_number('t_variation', t_variation, do_include_zero = False)
1595
+ ParameterValidator.validate_number_between('limit', limit, time_interval.start, time_interval.end, False, False)
1596
+
1597
+ new_start = time_interval.start + t_variation
1598
+ limit = (
1599
+ time_interval.end
1600
+ if limit is None else
1601
+ limit
1602
+ )
1603
+
1604
+ if new_start >= time_interval.end:
1605
+ raise Exception('The "t_variation" value provided makes the new end value be lower than the "end" value.')
1606
+
1607
+ if new_start >= limit:
1608
+ raise Exception('The "t_variation" value provided makes the new end value be greater than the "limit" value provided.')
1609
+
1610
+ return (
1611
+ TimeInterval(
1612
+ start = time_interval.start,
1613
+ end = new_start
1614
+ ),
1615
+ TimeInterval(
1616
+ start = new_start,
1617
+ end = time_interval.end
1618
+ )
1619
+ )
1620
+
1621
+ @staticmethod
1622
+ # @parameter_to_time_interval('time_interval')
1623
+ @parameter_to_new_time_interval('time_interval')
1624
+ def from_to(
1625
+ time_interval: 'TimeInterval',
1626
+ start: Number,
1627
+ end: Number
1628
+ ) -> tuple[Union['TimeInterval', None], Union['TimeInterval', None], Union['TimeInterval', None], int]:
1629
+ """
1630
+ Cut a segment from the given `start` to the also provided
1631
+ `end` time moments of the `time_interval` passed as
1632
+ parameter.
1633
+
1634
+ This method will return a tuple of 3 elements including the
1635
+ segments created by cutting this time interval in the order
1636
+ they were generated, but also having the 4th element always
1637
+ as the index of the one specifically requested by the user.
1638
+ The tuple will include all the segments at the begining and
1639
+ the rest will be None (unless the 4th one, which is the
1640
+ index).
1641
+
1642
+ Examples below:
1643
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
1644
+ will generate `((2, 3), (3, 4), (4, 5), 1)`.
1645
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
1646
+ will generate `((2, 4), (4, 5), None, 0)`.
1647
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
1648
+ will generate `((2, 4), (4, 5), None, 1)`.
1649
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
1650
+ will generate `((2, 5), None, None, 0)`.
1651
+
1652
+ As you can see, the result could be the same in different
1653
+ situations, but it's up to you (and the specific method in
1654
+ which you are calling to this one) to choose the tuple you
1655
+ want to return.
1656
+ """
1657
+ time_interval._validate_t(start, do_include_start = True)
1658
+ time_interval._validate_t(end, do_include_end = True)
1659
+
1660
+ return (
1661
+ # TODO: What about this case, should we raise except (?)
1662
+ (
1663
+ time_interval.copy,
1664
+ None,
1665
+ None,
1666
+ 0
1667
+ )
1668
+ if (
1669
+ start == time_interval.start and
1670
+ end == time_interval.end
1671
+ ) else
1672
+ (
1673
+ TimeInterval(
1674
+ start = time_interval.start,
1675
+ end = end
1676
+ ),
1677
+ TimeInterval(
1678
+ start = end,
1679
+ end = time_interval.end
1680
+ ),
1681
+ None,
1682
+ 0
1683
+ )
1684
+ if start == time_interval.start else
1685
+ (
1686
+ TimeInterval(
1687
+ start = time_interval.start,
1688
+ end = start
1689
+ ),
1690
+ TimeInterval(
1691
+ start = start,
1692
+ end = time_interval.end
1693
+ ),
1694
+ None,
1695
+ 1
1696
+ )
1697
+ if end == time_interval.end else
1698
+ (
1699
+ TimeInterval(
1700
+ start = time_interval.start,
1701
+ end = start
1702
+ ),
1703
+ TimeInterval(
1704
+ start = start,
1705
+ end = end
1706
+ ),
1707
+ TimeInterval(
1708
+ start = end,
1709
+ end = time_interval.end
1710
+ ),
1711
+ 1
1712
+ )
1713
+ )
1714
+
1715
+ @staticmethod
1716
+ # @parameter_to_time_interval('time_interval')
1717
+ @parameter_to_new_time_interval('time_interval')
1718
+ def split(
1719
+ time_interval: 'TimeInterval',
1720
+ t: Number,
1721
+ ) -> tuple['TimeInterval', 'TimeInterval']:
1722
+ """
1723
+ Split the interval at the provided `t` time moment and
1724
+ get the 2 new time intervals as a result (as a tuple).
1725
+
1726
+ This method will raise an exception if the `t` value
1727
+ provided is a limit value (or above).
1728
+
1729
+ Examples below:
1730
+ - A time interval of `[2, 5)` cut with `t=3` will generate
1731
+ `((2, 3), (3, 5))`.
1732
+ - A time interval of `[2, 5)` cut with `t=4` will generate
1733
+ `((2, 4), (4, 5))`.
1734
+ - A time interval of `[2, 5)` cut with `t>=5` will raise
1735
+ exception.
1736
+ - A time interval of `[2, 5)` cut with `t<=2` will raise
1737
+ exception.
1738
+ """
1739
+ if (
1740
+ t <= time_interval.start or
1741
+ t >= time_interval.end
1742
+ ):
1743
+ raise Exception('The "t" value provided is not a valid value as it is a limit (or more than a limit).')
1744
+
1745
+ return (
1746
+ TimeInterval(
1747
+ start = time_interval.start,
1748
+ end = t
1749
+ ),
1750
+ TimeInterval(
1751
+ start = t,
1752
+ end = time_interval.end
1753
+ )
1754
+ )
1755
+
1756
+ # TODO: Maybe add some 'extend' functionality
1757
+
1758
+ # # TODO: This method is interesting if we don't want only to
1759
+ # # trim but also to extend, so we can use the limits.
1760
+ # @staticmethod
1761
+ # def trim_end(
1762
+ # time_interval: TimeIntervalType,
1763
+ # t_variation: Number,
1764
+ # lower_limit: Number = 0.0,
1765
+ # upper_limit: Union[Number, None] = None,
1766
+ # do_include_lower_limit: bool = True,
1767
+ # do_include_upper_limit: bool = True
1768
+ # ) -> Union['TimeInterval', 'TimeInterval']:
1769
+ # """
1770
+ # Get a tuple containing the 2 new `TimeInterval` instances
1771
+ # generated by trimming the `time_interval` provided from
1772
+ # the `end` and at the `t` time moment provided. The first
1773
+ # value is the remaining, and the second one is the requested
1774
+ # by the user.
1775
+
1776
+ # This will raise an exception if the new `end` becomes a
1777
+ # value under the `lower_limit` provided, above the
1778
+ # `upper_limit` given (if given) or even below the time
1779
+ # interval `start` value. The limits will be included or not
1780
+ # according to the boolean parameters.
1781
+ # """
1782
+ # new_end = time_interval.end + t_variation
1783
+
1784
+ # if new_end <= time_interval.start:
1785
+ # raise Exception('The "t_variation" value provided makes the new end value be lower than the "start" value.')
1786
+
1787
+ # is_under_lower_limit = (
1788
+ # new_end < lower_limit
1789
+ # if do_include_lower_limit else
1790
+ # new_end <= lower_limit
1791
+ # )
1792
+
1793
+ # is_over_upper_limit = (
1794
+ # upper_limit is None and
1795
+ # (
1796
+ # new_end > upper_limit
1797
+ # if do_include_upper_limit else
1798
+ # new_end >= upper_limit
1799
+ # )
1800
+ # )
1801
+
1802
+ # if (
1803
+ # is_under_lower_limit or
1804
+ # is_over_upper_limit
1805
+ # ):
1806
+ # raise Exception('The "t_variation" value provided makes the new end value be out of the limits.')
1807
+
1808
+ # return (
1809
+ # TimeInterval(
1810
+ # start = time_interval.start,
1811
+ # end = new_end
1812
+ # ),
1813
+ # TimeInterval(
1814
+ # start = new_end,
1815
+ # end = time_interval.end
1816
+ # )
1817
+ # )
1818
+