Repository: nodatime/nodatime (csharp)

Issue:
Effective Instant serialization/deserialization
I've faced a problem with effective serialization and deserialization `Instant` for MessagePack. `Instant` has a precision of nanoseconds. The best safe way for serialization is getting nanoseconds amount (and create an instance of `Instant` from nanoseconds for deserialization). But the minimum accessible unit of time from Instant is ticks (`.ToUnixTimeTicks()`). 
I can't use ticks, because value can be truncated (`Instant.MaxValue` for example)

Now there are a few terrible ways to serialize/deserialize an Instant:
- with InstantPattern (as string with format pattern). It's a slowest way, and uses a lot of memory
- with code smells way: getting `Duration` with `Instant.MinValue - instant` for serialization, and getting `Instant` with 'Instant.Add(Instant.MinValue, duration)' for deserialization
- with dirty hacks for getting access to private fields: Reflection, Linq Expressions

I would like to propose change access modifier for `Duration` field to public. And make a constructor with `Duration` as argument public. It will help with serialization and make `Instant` more flexible and predictable with nanoseconds manipulating.

Do you have any thoughts about this?

Pull request:
Fixes #1644.

Patch:
diff --git a/src/NodaTime/Instant.cs b/src/NodaTime/Instant.cs
index c2430ee34..a032c2561 100644
--- a/src/NodaTime/Instant.cs
+++ b/src/NodaTime/Instant.cs
@@ -655,6 +655,31 @@ public static Instant FromUnixTimeTicks(long ticks)
         public long ToUnixTimeSeconds() =>
             duration.FloorDays * (long) SecondsPerDay + duration.NanosecondOfFloorDay / NanosecondsPerSecond;
 
+        /// <summary>
+        /// Gets the number of seconds since the Unix epoch, along with remaining nanoseconds.
+        /// Negative values for seconds represent instants before the Unix epoch.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// If the number of nanoseconds in this instant is not an exact number of seconds
+        /// the seconds part of the returned value is truncated towards the start of time, ensuring that
+        /// the nanoseconds part is always non-negative. The seconds part of the returned value is always
+        /// the same as would be returned by <see cref="ToUnixTimeSeconds"/>.
+        /// </para>
+        /// <para>
+        /// The inverse of this operation is to first call <see cref="FromUnixTimeSeconds(long)"/> and then
+        /// call <see cref="PlusNanoseconds(long)"/> on the returned <see cref="Instant"/>.
+        /// </para>
+        /// </remarks>
+        /// <value>The number of seconds and remaining nanoseconds since the Unix epoch.</value>
+        [Pure]
+        [TestExemption(TestExemptionCategory.ConversionName)]
+        public (long seconds, int nanoseconds) ToUnixTimeSecondsAndNanoseconds()
+        {
+            var secondOfDay = Math.DivRem(duration.NanosecondOfFloorDay, NanosecondsPerSecond, out var nanosecondOfSecond);
+            return (((long) SecondsPerDay * duration.FloorDays) + secondOfDay, unchecked((int) nanosecondOfSecond));
+        }
+
         /// <summary>
         /// Gets the number of milliseconds since the Unix epoch. Negative values represent instants before the Unix epoch.
         /// </summary>


Test patch:
diff --git a/src/NodaTime.Test/InstantTest.cs b/src/NodaTime.Test/InstantTest.cs
index 10fea1d15..caa942474 100644
--- a/src/NodaTime.Test/InstantTest.cs
+++ b/src/NodaTime.Test/InstantTest.cs
@@ -155,6 +155,29 @@ public void ToUnixTimeSeconds(long milliseconds, int expectedSeconds)
             Assert.AreEqual(expectedSeconds, instant.ToUnixTimeSeconds());
         }
 
+        // The fact that all the expected nanosecond values are multiples of
+        // a million is slightly unfortunate, but the logic is so simple that
+        // it's not worth going to extreme lengths to test more fine-grained values.
+        [Test]
+        [TestCase(-1500, -2, 500_000_000)]
+        [TestCase(-1001, -2, 999_000_000)]
+        [TestCase(-1000, -1, 0)]
+        [TestCase(-999, -1, 1_000_000)]
+        [TestCase(-500, -1, 500_000_000)]
+        [TestCase(0, 0, 0)]
+        [TestCase(500, 0, 500_000_000)]
+        [TestCase(999, 0, 999_000_000)]
+        [TestCase(1000, 1, 0)]
+        [TestCase(1001, 1, 1_000_000)]
+        [TestCase(1500, 1, 500_000_000)]
+        public void ToUnixTimeSecondsAndNanoseconds(long milliseconds, int expectedSeconds, int expectedNanoseconds)
+        {
+            var instant = Instant.FromUnixTimeMilliseconds(milliseconds);
+            var (actualSeconds, actualNanoseconds) = instant.ToUnixTimeSecondsAndNanoseconds();
+            Assert.AreEqual(expectedSeconds, actualSeconds);
+            Assert.AreEqual(expectedNanoseconds, actualNanoseconds);
+        }
+
         [Test]
         [TestCase(-15000, -2)]
         [TestCase(-10001, -2)]