From 0ec1ef3b0fbbec05279ef3d83e2d9ef970e654a8 Mon Sep 17 00:00:00 2001 From: rick-slin <99432032+rick-slin@users.noreply.github.com> Date: Sat, 4 May 2024 00:45:54 -0400 Subject: [PATCH] Added support for centisecond and decimillisecond precisions (#282) --- dfdatetime/definitions.py | 6 + dfdatetime/precisions.py | 117 ++++++++++++++++++- tests/precisions.py | 57 +++++++++ tests/time_elements.py | 239 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 417 insertions(+), 2 deletions(-) diff --git a/dfdatetime/definitions.py b/dfdatetime/definitions.py index 46200f4..e8c9a9c 100644 --- a/dfdatetime/definitions.py +++ b/dfdatetime/definitions.py @@ -13,8 +13,12 @@ DECISECONDS_PER_SECOND = 10 +CENTISECONDS_PER_SECOND = 100 + MILLISECONDS_PER_SECOND = 1000 +DECIMICROSECONDS_PER_SECOND = 10000 + MICROSECONDS_PER_DAY = 86400000000 MICROSECONDS_PER_SECOND = 1000000 MICROSECONDS_PER_DECISECOND = 100000 @@ -23,7 +27,9 @@ NANOSECONDS_PER_DAY = 86400000000000 NANOSECONDS_PER_SECOND = 1000000000 NANOSECONDS_PER_DECISECOND = 100000000 +NANOSECONDS_PER_CENTISECOND = 10000000 NANOSECONDS_PER_MILLISECOND = 1000000 +NANOSECONDS_PER_DECIMILISECOND = 100000 NANOSECONDS_PER_MICROSECOND = 1000 PRECISION_1_DAY = '1d' diff --git a/dfdatetime/precisions.py b/dfdatetime/precisions.py index 108ca1b..6491180 100644 --- a/dfdatetime/precisions.py +++ b/dfdatetime/precisions.py @@ -25,8 +25,8 @@ def CopyNanosecondsToFractionOfSecond(cls, nanoseconds): nanoseconds (int): number of nanoseconds. Returns: - decimal.Decimal: fraction of second, which must be a value between 0.0 - and 1.0. + decimal.Decimal: fraction of second, which must be a value between 0.0 and + 1.0. """ raise NotImplementedError() @@ -99,6 +99,60 @@ def CopyToDateTimeString(cls, time_elements_tuple, fraction_of_second): f'{hours:02d}:{minutes:02d}:{seconds:02d}') +class CentisecondsPrecisionHelper(DateTimePrecisionHelper): + """Centiseconds (10 ms) precision helper.""" + + @classmethod + def CopyNanosecondsToFractionOfSecond(cls, nanoseconds): + """Copies the number of nanoseconds to a fraction of second value. + + Args: + nanoseconds (int): number of nanoseconds. + + Returns: + decimal.Decimal: fraction of second, which must be a value between 0.0 and + 1.0. + + Raises: + ValueError: if the number of nanoseconds is out of bounds. + """ + if nanoseconds < 0 or nanoseconds >= definitions.NANOSECONDS_PER_SECOND: + raise ValueError( + f'Number of nanoseconds value: {nanoseconds:d} out of bounds.') + + centiseconds, _ = divmod( + nanoseconds, definitions.NANOSECONDS_PER_CENTISECOND) + return decimal.Decimal(centiseconds) / definitions.CENTISECONDS_PER_SECOND + + @classmethod + def CopyToDateTimeString(cls, time_elements_tuple, fraction_of_second): + """Copies the time elements and fraction of second to a string. + + Args: + time_elements_tuple (tuple[int, int, int, int, int, int]): + time elements, contains year, month, day of month, hours, minutes and + seconds. + fraction_of_second (decimal.Decimal): fraction of second, which must be a + value between 0.0 and 1.0. + + Returns: + str: date and time value formatted as: + YYYY-MM-DD hh:mm:ss.## + + Raises: + ValueError: if the fraction of second is out of bounds. + """ + if fraction_of_second < 0.0 or fraction_of_second >= 1.0: + raise ValueError( + f'Fraction of second value: {fraction_of_second:f} out of bounds.') + + year, month, day_of_month, hours, minutes, seconds = time_elements_tuple + centiseconds = int(fraction_of_second * definitions.CENTISECONDS_PER_SECOND) + + return (f'{year:04d}-{month:02d}-{day_of_month:02d} ' + f'{hours:02d}:{minutes:02d}:{seconds:02d}.{centiseconds:02d}') + + class MillisecondsPrecisionHelper(DateTimePrecisionHelper): """Milliseconds precision helper.""" @@ -153,6 +207,63 @@ def CopyToDateTimeString(cls, time_elements_tuple, fraction_of_second): f'{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}') +class DecimillisecondsPrecisionHelper(DateTimePrecisionHelper): + """Decimilliseconds (100 microseconds) precision helper.""" + + @classmethod + def CopyNanosecondsToFractionOfSecond(cls, nanoseconds): + """Copies the number of nanoseconds to a fraction of second value. + + Args: + nanoseconds (int): number of nanoseconds. + + Returns: + decimal.Decimal: fraction of second, which must be a value between 0.0 + and 1.0. + + Raises: + ValueError: if the number of nanoseconds is out of bounds. + """ + if nanoseconds < 0 or nanoseconds >= definitions.NANOSECONDS_PER_SECOND: + raise ValueError( + f'Number of nanoseconds value: {nanoseconds:d} out of bounds.') + + decimiliseconds, _ = divmod( + nanoseconds, definitions.NANOSECONDS_PER_DECIMILISECOND) + return ( + decimal.Decimal(decimiliseconds) / + definitions.DECIMICROSECONDS_PER_SECOND) + + @classmethod + def CopyToDateTimeString(cls, time_elements_tuple, fraction_of_second): + """Copies the time elements and fraction of second to a string. + + Args: + time_elements_tuple (tuple[int, int, int, int, int, int]): + time elements, contains year, month, day of month, hours, minutes and + seconds. + fraction_of_second (decimal.Decimal): fraction of second, which must be a + value between 0.0 and 1.0. + + Returns: + str: date and time value formatted as: + YYYY-MM-DD hh:mm:ss.#### + + Raises: + ValueError: if the fraction of second is out of bounds. + """ + if fraction_of_second < 0.0 or fraction_of_second >= 1.0: + raise ValueError( + f'Fraction of second value: {fraction_of_second:f} out of bounds.') + + year, month, day_of_month, hours, minutes, seconds = time_elements_tuple + decimicroseconds = int( + fraction_of_second * definitions.DECIMICROSECONDS_PER_SECOND) + + return (f'{year:04d}-{month:02d}-{day_of_month:02d} ' + f'{hours:02d}:{minutes:02d}:{seconds:02d}.{decimicroseconds:04d}') + + class MicrosecondsPrecisionHelper(DateTimePrecisionHelper): """Microseconds precision helper.""" @@ -263,6 +374,8 @@ class PrecisionHelperFactory(object): """Date time precision helper factory.""" _PRECISION_CLASSES = { + definitions.PRECISION_10_MILLISECONDS: CentisecondsPrecisionHelper, + definitions.PRECISION_100_MICROSECONDS: DecimillisecondsPrecisionHelper, definitions.PRECISION_1_MICROSECOND: MicrosecondsPrecisionHelper, definitions.PRECISION_1_MILLISECOND: MillisecondsPrecisionHelper, definitions.PRECISION_1_NANOSECOND: NanosecondsPrecisionHelper, diff --git a/tests/precisions.py b/tests/precisions.py index fcd558a..ec08290 100644 --- a/tests/precisions.py +++ b/tests/precisions.py @@ -56,6 +56,35 @@ def testCopyToDateTimeString(self): precision_helper.CopyToDateTimeString((2018, 1, 2, 19, 45, 12), 4.123456) +class CentisecondsPrevisionHelperTest(unittest.TestCase): + """Tests for the centiseconds prevision helper.""" + + def testCopyNanosecondsToFractionOfSecond(self): + """Tests the CopyNanosecondsToFractionOfSecond function.""" + precision_helper = precisions.CentisecondsPrecisionHelper + + fraction_of_second = precision_helper.CopyNanosecondsToFractionOfSecond( + 123456789) + self.assertEqual(fraction_of_second, decimal.Decimal('0.12')) + + with self.assertRaises(ValueError): + precision_helper.CopyNanosecondsToFractionOfSecond(-1) + + with self.assertRaises(ValueError): + precision_helper.CopyNanosecondsToFractionOfSecond(1000000000) + + def testCopyToDateTimeString(self): + """Tests the CopyToDateTimeString function.""" + precision_helper = precisions.CentisecondsPrecisionHelper + + date_time_string = precision_helper.CopyToDateTimeString( + (2018, 1, 2, 19, 45, 12), 0.123456) + self.assertEqual(date_time_string, '2018-01-02 19:45:12.12') + + with self.assertRaises(ValueError): + precision_helper.CopyToDateTimeString((2018, 1, 2, 19, 45, 12), 4.123456) + + class MillisecondsPrecisionHelperTest(unittest.TestCase): """Tests for the milliseconds precision helper.""" @@ -85,6 +114,34 @@ def testCopyToDateTimeString(self): precision_helper.CopyToDateTimeString((2018, 1, 2, 19, 45, 12), 4.123456) +class DeciMillisecondsPrevisionHelperTest(unittest.TestCase): + """Tests for the decimilliseconds precision helper.""" + def testCopyNanosecondsToFractionOfSecond(self): + """Tests the CopyNanosecondsToFractionOfSecond function.""" + precision_helper = precisions.DecimillisecondsPrecisionHelper + + fraction_of_second = precision_helper.CopyNanosecondsToFractionOfSecond( + 123456789) + self.assertEqual(fraction_of_second, decimal.Decimal('0.1234')) + + with self.assertRaises(ValueError): + precision_helper.CopyNanosecondsToFractionOfSecond(-1) + + with self.assertRaises(ValueError): + precision_helper.CopyNanosecondsToFractionOfSecond(1000000000) + + def testCopyToDateTimeString(self): + """Tests the CopyToDateTimeString function.""" + precision_helper = precisions.DecimillisecondsPrecisionHelper + + date_time_string = precision_helper.CopyToDateTimeString( + (2018, 1, 2, 19, 45, 12), 0.123456) + self.assertEqual(date_time_string, '2018-01-02 19:45:12.1234') + + with self.assertRaises(ValueError): + precision_helper.CopyToDateTimeString((2018, 1, 2, 19, 45, 12), 4.123456) + + class MicrosecondsPrecisionHelperTest(unittest.TestCase): """Tests for the microseconds precision helper.""" diff --git a/tests/time_elements.py b/tests/time_elements.py index 980d820..6efb3f3 100644 --- a/tests/time_elements.py +++ b/tests/time_elements.py @@ -6,6 +6,7 @@ import decimal import unittest +from dfdatetime import definitions from dfdatetime import time_elements @@ -1604,5 +1605,243 @@ def testNewFromDeltaAndYear(self): time_elements_object.NewFromDeltaAndYear(2009) +class TimeElementsWithFractionOfSeconds(unittest.TestCase): + """Tests for the time elements with fractions of seconds.""" + + # pylint: disable=protected-access + + def testInitialize(self): + """Tests the initialization function.""" + time_elements_object = time_elements.TimeElementsWithFractionOfSecond() + self.assertIsNotNone(time_elements_object) + + expected_time_elements_tuple = (2010, 8, 12, 20, 6, 31) + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal(0.87), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + self.assertIsNotNone(time_elements_object) + self.assertEqual( + time_elements_object._time_elements_tuple, expected_time_elements_tuple) + self.assertEqual(time_elements_object.fraction_of_second, 0.87) + + expected_time_elements_tuple = (2010, 8, 12, 20, 6, 31) + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal(0.8742), + precision=definitions.PRECISION_100_MICROSECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + self.assertIsNotNone(time_elements_object) + self.assertEqual( + time_elements_object._time_elements_tuple, expected_time_elements_tuple) + self.assertEqual(time_elements_object.fraction_of_second, 0.8742) + + with self.assertRaises(ValueError): + time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('1.87'), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + + with self.assertRaises(ValueError): + time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('-1'), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + + def testGetNormalizedTimestamp(self): + """Tests the _GetNormalizedTimestamp function.""" + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.87'), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + normalized_timestamp = time_elements_object._GetNormalizedTimestamp() + self.assertEqual(normalized_timestamp, decimal.Decimal('1281643591.87')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.87'), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31), + time_zone_offset=60) + normalized_timestamp = time_elements_object._GetNormalizedTimestamp() + self.assertEqual(normalized_timestamp, decimal.Decimal('1281639991.87')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.87'), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + time_elements_object.time_zone_offset = 60 + normalized_timestamp = time_elements_object._GetNormalizedTimestamp() + self.assertEqual(normalized_timestamp, decimal.Decimal('1281639991.87')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.8724'), + precision=definitions.PRECISION_100_MICROSECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + normalized_timestamp = time_elements_object._GetNormalizedTimestamp() + self.assertEqual(normalized_timestamp, decimal.Decimal('1281643591.8724')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond() + normalized_timestamp = time_elements_object._GetNormalizedTimestamp() + self.assertIsNone(normalized_timestamp) + + def testCopyFromDateTimeValues(self): + """Tests the _CopyFromDateTimeValues function.""" + date_time_values = { + 'year': 2010, + 'month': 8, + 'day_of_month': 12, + 'hours': 21, + 'minutes': 6, + 'seconds': 31, + 'nanoseconds': 123456789, + 'time_zone_offset': 60} + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + precision=definitions.PRECISION_10_MILLISECONDS) + time_elements_object._CopyFromDateTimeValues(date_time_values) + + self.assertEqual( + time_elements_object._time_elements_tuple, (2010, 8, 12, 21, 6, 31)) + self.assertEqual( + time_elements_object.fraction_of_second, decimal.Decimal('0.12')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + precision=definitions.PRECISION_1_MILLISECOND) + time_elements_object._CopyFromDateTimeValues(date_time_values) + self.assertEqual( + time_elements_object.fraction_of_second, decimal.Decimal('0.123')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + precision=definitions.PRECISION_100_MICROSECONDS) + time_elements_object._CopyFromDateTimeValues(date_time_values) + self.assertEqual( + time_elements_object.fraction_of_second, decimal.Decimal('0.1234')) + + def testCopyFromDatetime(self): + """Tests the CopyFromDatetime function.""" + datetime_object = datetime.datetime(2010, 8, 12, 21, 6, 31, 546875) + expected_time_elements_tuple = (2010, 8, 12, 21, 6, 31) + expected_number_of_seconds = 1281647191 + expected_fraction_of_second = decimal.Decimal('0.54') + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + precision=definitions.PRECISION_10_MILLISECONDS) + time_elements_object.CopyFromDatetime(datetime_object) + + self.assertEqual( + time_elements_object._time_elements_tuple, expected_time_elements_tuple) + self.assertEqual( + time_elements_object._number_of_seconds, expected_number_of_seconds) + self.assertEqual( + time_elements_object.fraction_of_second, expected_fraction_of_second) + self.assertTrue(time_elements_object.is_local_time) + + datetime_object = datetime.datetime( + 2010, 8, 12, 21, 6, 31, 546875, tzinfo=datetime.timezone.utc) + time_elements_object.CopyFromDatetime(datetime_object) + self.assertEqual( + time_elements_object._time_elements_tuple, expected_time_elements_tuple) + self.assertEqual( + time_elements_object._number_of_seconds, expected_number_of_seconds) + self.assertEqual( + time_elements_object.fraction_of_second, expected_fraction_of_second) + self.assertFalse(time_elements_object.is_local_time) + + def testCopyFromStringTuple(self): + """Tests the CopyFromStringTuple function.""" + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + precision=definitions.PRECISION_10_MILLISECONDS) + + expected_time_elements_tuple = (2010, 8, 12, 20, 6, 31) + expected_fraction_of_second = decimal.Decimal('0.46') + time_elements_object.CopyFromStringTuple( + time_elements_tuple=('2010', '8', '12', '20', '6', '31', '0.46')) + + self.assertEqual( + time_elements_object._time_elements_tuple, expected_time_elements_tuple) + self.assertEqual( + time_elements_object.fraction_of_second, expected_fraction_of_second) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + precision=definitions.PRECISION_100_MICROSECONDS) + time_elements_object.CopyFromStringTuple( + time_elements_tuple=('2010', '8', '12', '20', '6', '31', '0.4671')) + self.assertEqual( + time_elements_object.fraction_of_second, decimal.Decimal('0.4671')) + + with self.assertRaises(ValueError): + time_elements_object.CopyFromStringTuple( + time_elements_tuple=('2010', '8', '12', '20', '6', '31')) + + with self.assertRaises(ValueError): + time_elements_object.CopyFromStringTuple( + time_elements_tuple=('2010', '8', '12', '20', '6', '31', '96')) + + def testCopyToDateTimeString(self): + """Tests the CopyToDateTimeString function.""" + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.87'), + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + + date_time_string = time_elements_object.CopyToDateTimeString() + self.assertEqual(date_time_string, '2010-08-12 20:06:31.87') + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.874'), + precision=definitions.PRECISION_1_MILLISECOND, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + + date_time_string = time_elements_object.CopyToDateTimeString() + self.assertEqual(date_time_string, '2010-08-12 20:06:31.874') + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.8741'), + precision=definitions.PRECISION_100_MICROSECONDS, + time_elements_tuple=(2010, 8, 12, 20, 6, 31)) + + date_time_string = time_elements_object.CopyToDateTimeString() + self.assertEqual(date_time_string, '2010-08-12 20:06:31.8741') + + def testNewFromDeltaAndDate(self): + """Tests the NewFromDeltaAndDate function.""" + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.87'), + is_delta=True, + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(1, 0, 0, 20, 6, 31)) + + new_time_elements_object = time_elements_object.NewFromDeltaAndDate( + 2009, 1, 12) + self.assertFalse(new_time_elements_object.is_delta) + self.assertEqual(new_time_elements_object.year, 2010) + self.assertEqual(new_time_elements_object.month, 1) + self.assertEqual(new_time_elements_object.day_of_month, 12) + self.assertEqual( + new_time_elements_object.fraction_of_second, decimal.Decimal('0.87')) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + time_elements_tuple=(1, 0, 0, 20, 6, 31)) + + with self.assertRaises(ValueError): + time_elements_object.NewFromDeltaAndDate(2009, 1, 12) + + time_elements_object = time_elements.TimeElementsWithFractionOfSecond() + + with self.assertRaises(ValueError): + time_elements_object.NewFromDeltaAndDate(2009, 1, 12) + + def testNewFromDeltaAndYear(self): + """Tests the NewFromDeltaAndYear function.""" + time_elements_object = time_elements.TimeElementsWithFractionOfSecond( + fraction_of_second=decimal.Decimal('0.87'), + is_delta=True, + precision=definitions.PRECISION_10_MILLISECONDS, + time_elements_tuple=(1, 8, 12, 20, 6, 31)) + + new_time_elements_object = time_elements_object.NewFromDeltaAndYear(2009) + self.assertFalse(new_time_elements_object.is_delta) + self.assertEqual(new_time_elements_object.year, 2010) + + if __name__ == '__main__': unittest.main()