Repository: log2timeline/dfdatetime (language: python)

Issue:
Add support for Windows ticks
https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks?view=net-6.0


Discussion / hints:

Fix patch:
diff --git a/dfdatetime/__init__.py b/dfdatetime/__init__.py
index 3fcb54f..f5d81bd 100644
--- a/dfdatetime/__init__.py
+++ b/dfdatetime/__init__.py
@@ -9,6 +9,7 @@
 from dfdatetime import apfs_time
 from dfdatetime import cocoa_time
 from dfdatetime import delphi_date_time
+from dfdatetime import dotnet_datetime
 from dfdatetime import fat_date_time
 from dfdatetime import filetime
 from dfdatetime import hfs_time
diff --git a/dfdatetime/dotnet_datetime.py b/dfdatetime/dotnet_datetime.py
new file mode 100644
index 0000000..43b184e
--- /dev/null
+++ b/dfdatetime/dotnet_datetime.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+""".NET DateTime implementation."""
+
+import decimal
+
+from dfdatetime import definitions
+from dfdatetime import factory
+from dfdatetime import interface
+
+
+class DotNetDateTimeEpoch(interface.DateTimeEpoch):
+  """.NET DateTime epoch."""
+
+  def __init__(self):
+    """Initializes a .NET DateTime epoch."""
+    super(DotNetDateTimeEpoch, self).__init__(1, 1, 1)
+
+
+class DotNetDateTime(interface.DateTimeValues):
+  """.NET DateTime ticks.
+
+  The .NET DateTime timestamp is a 64-bit signed integer that
+  contains the date and time as the number of 100 nanoseconds since
+  12:00 AM January 1, year 1 A.D. in the proleptic Gregorian Calendar.
+  """
+  _EPOCH = DotNetDateTimeEpoch()
+
+  # The difference between January 1, 0001 and January 1, 1970 in seconds.
+  _DOTNET_TO_POSIX_BASE =  (
+      ((1969 * 365) + (1969 // 4) - (1969 // 100) + (1969 // 400)) *
+      definitions.SECONDS_PER_DAY)
+
+  def __init__(self, timestamp=None):
+    """Initializes a .NET DateTime timestamp.
+
+    Args:
+      timestamp (Optional[int]): .NET DateTime ticks.
+    """
+    super(DotNetDateTime, self).__init__(time_zone_offset=0)
+    self._precision = definitions.PRECISION_100_NANOSECONDS
+    self._timestamp = timestamp or 0
+
+  @property
+  def timestamp(self):
+    """integer: .NET DateTime timestamp or None if not set."""
+    return self._timestamp
+
+  def _GetNormalizedTimestamp(self):
+    """Retrieves the normalized timestamp.
+
+    Returns:
+      decimal.Decimal: normalized timestamp, which contains the number of
+          seconds since January 1, 1970 00:00:00 and a fraction of second used
+          for increased precision, or None if the normalized timestamp cannot be
+          determined.
+    """
+    if self._normalized_timestamp is None:
+      if self._timestamp is not None:
+        self._normalized_timestamp = (
+            decimal.Decimal(self._timestamp) / self._100_NANOSECONDS_PER_SECOND)
+        self._normalized_timestamp -= self._DOTNET_TO_POSIX_BASE
+
+        if self._time_zone_offset:
+          self._normalized_timestamp -= self._time_zone_offset * 60
+
+    return self._normalized_timestamp
+
+  def CopyFromDateTimeString(self, time_string):
+    """Copies a .NET DateTime timestamp from a string.
+
+    Args:
+      time_string (str): date and time value formatted as:
+          YYYY-MM-DD hh:mm:ss.######[+-]##:##
+
+          Where # are numeric digits ranging from 0 to 9 and the seconds
+          fraction can be either 3 or 6 digits. The time of day, seconds
+          fraction and time zone offset are optional. The default time zone
+          is UTC.
+
+    Raises:
+      ValueError: if the time string is invalid or not supported.
+    """
+    date_time_values = self._CopyDateTimeFromString(time_string)
+
+    year = date_time_values.get('year', 0)
+    month = date_time_values.get('month', 0)
+    day_of_month = date_time_values.get('day_of_month', 0)
+    hours = date_time_values.get('hours', 0)
+    minutes = date_time_values.get('minutes', 0)
+    seconds = date_time_values.get('seconds', 0)
+    microseconds = date_time_values.get('microseconds', 0)
+    time_zone_offset = date_time_values.get('time_zone_offset', 0)
+
+    if year > 9999:
+      raise ValueError('Unsupported year value: {0:d}.'.format(year))
+
+    timestamp = self._GetNumberOfSecondsFromElements(
+        year, month, day_of_month, hours, minutes, seconds)
+    timestamp += self._DOTNET_TO_POSIX_BASE
+    timestamp *= definitions.MICROSECONDS_PER_SECOND
+    timestamp += microseconds
+    timestamp *= self._100_NANOSECONDS_PER_MICROSECOND
+
+    self._normalized_timestamp = None
+    self._timestamp = timestamp
+    self._time_zone_offset = time_zone_offset
+
+  def CopyToDateTimeString(self):
+    """Copies the .NET DateTime timestamp to a date and time string.
+
+    Returns:
+      str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.######" or
+          None if the timestamp is missing.
+    """
+    if (self._timestamp is None or self._timestamp < 0 or
+        self._timestamp > self._UINT64_MAX):
+      return None
+
+    timestamp, remainder = divmod(
+        self._timestamp, self._100_NANOSECONDS_PER_SECOND)
+    number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
+
+    year, month, day_of_month = self._GetDateValuesWithEpoch(
+        number_of_days, self._EPOCH)
+
+    return '{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}.{6:07d}'.format(
+        year, month, day_of_month, hours, minutes, seconds, remainder)
+
+
+factory.Factory.RegisterDateTimeValues(DotNetDateTime)
diff --git a/docs/sources/Date-and-time-values.md b/docs/sources/Date-and-time-values.md
index 626341b..e1c70dd 100644
--- a/docs/sources/Date-and-time-values.md
+++ b/docs/sources/Date-and-time-values.md
@@ -72,6 +72,25 @@ occurred during that specific hour.
 * [File Times](https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times)
 * [Precision and accuracy of DateTime](https://docs.microsoft.com/en-us/archive/blogs/ericlippert/precision-and-accuracy-of-datetime), by Eric Lippert, April 8, 2010
 
+## .NET DateTime
+### Characteristics
+
+Attribute | Description
+--- | ---
+Supported date range | 0001-01-01 00:00:00 through 9999-12-31 23:59:59
+Storage granularity | 100 nanoseconds
+Time zone | externally represented, typically UTC
+
+### Format
+
+Offset | Size | Description
+0 | 4 or 8 | timestamp, little endian integer value containing the number of 100 nanosecond intervals since January 1, 0001 00:00:00.0000000
+
+### Also see
+
+* [Microsoft: DateTime struct](https://docs.microsoft.com/en-us/dotnet/api/system.datetime?view=net-6.0)
+* [Microsoft: DateTime.Ticks](https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks)
+
 ## APFS timestamp
 ### Characteristics
 


Test patch:
diff --git a/tests/dotnet_datetime.py b/tests/dotnet_datetime.py
new file mode 100644
index 0000000..3884554
--- /dev/null
+++ b/tests/dotnet_datetime.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Tests for the .NET DateTime implementation."""
+
+import decimal
+import unittest
+
+from dfdatetime import dotnet_datetime
+
+
+class DotNetDateTimeEpochTest(unittest.TestCase):
+  """Tests for the .NET DateTime epoch."""
+
+  def testInitialize(self):
+    """Tests the __init__ function."""
+    dotnet_date_time_epoch = dotnet_datetime.DotNetDateTimeEpoch()
+    self.assertIsNotNone(dotnet_date_time_epoch)
+
+
+class DotNetDateTimeTest(unittest.TestCase):
+  """Tests for the ,NET DateTime timestamp."""
+
+  # pylint: disable=protected-access
+
+  def testProperties(self):
+    """Tests the properties."""
+    dotnet_date_time = dotnet_datetime.DotNetDateTime()
+    self.assertEqual(dotnet_date_time.timestamp, 0)
+
+    dotnet_date_time = dotnet_datetime.DotNetDateTime(
+        timestamp=637751130027210000)
+    self.assertEqual(dotnet_date_time.timestamp, 637751130027210000)
+
+  def testGetNormalizedTimestamp(self):
+    """Tests the _GetNormalizedTimestamp function."""
+    dotnet_date_time = dotnet_datetime.DotNetDateTime(
+        timestamp=637433719321230000)
+    expected_normalized_timestamp = decimal.Decimal(1607775132123) / 1000
+    normalized_timestamp = dotnet_date_time._GetNormalizedTimestamp()
+    self.assertEqual(normalized_timestamp, expected_normalized_timestamp)
+
+  def testCopyFromDateTimeString(self):
+    """Tests the CopyFromDateTimeString function."""
+    dotnet_date_time = dotnet_datetime.DotNetDateTime()
+
+    dotnet_date_time.CopyFromDateTimeString('2020-12-12')
+    self.assertEqual(dotnet_date_time._timestamp, 637433280000000000)
+    self.assertEqual(dotnet_date_time._time_zone_offset, 0)
+
+    dotnet_date_time.CopyFromDateTimeString('2020-12-12 12:12:12')
+    self.assertEqual(dotnet_date_time._timestamp, 637433719320000000)
+    self.assertEqual(dotnet_date_time._time_zone_offset, 0)
+
+    dotnet_date_time.CopyFromDateTimeString('2020-12-12 12:12:12.123')
+    self.assertEqual(dotnet_date_time._timestamp, 637433719321230000)
+    self.assertEqual(dotnet_date_time._time_zone_offset, 0)
+
+  def testCopyToDateTimeString(self):
+    """Tests the CopyToDateTimeString function."""
+    dotnet_date_time = dotnet_datetime.DotNetDateTime(
+        timestamp=637433280000000000)
+
+    dotnet_date_string = dotnet_date_time.CopyToDateTimeString()
+    self.assertEqual(dotnet_date_string, '2020-12-12 00:00:00.0000000')
+
+    dotnet_date_time = dotnet_datetime.DotNetDateTime(
+        timestamp=637433719320000000)
+
+    dotnet_date_string = dotnet_date_time.CopyToDateTimeString()
+    self.assertEqual(dotnet_date_string, '2020-12-12 12:12:12.0000000')
+
+    dotnet_date_time = dotnet_datetime.DotNetDateTime(
+        timestamp=637433719321230000)
+
+    dotnet_date_string = dotnet_date_time.CopyToDateTimeString()
+    self.assertEqual(dotnet_date_string, '2020-12-12 12:12:12.1230000')
+
+
+if __name__ == '__main__':
+  unittest.main()