Skip to content

Commit

Permalink
Add support to pre-epoch Unix time (JamesNK#2614)
Browse files Browse the repository at this point in the history
Co-authored-by: Max A. Kiselev <[email protected]>
Co-authored-by: James Newton-King <[email protected]>
  • Loading branch information
3 people authored May 15, 2022
1 parent 13717cf commit 7f78562
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 2 deletions.
87 changes: 87 additions & 0 deletions Src/Newtonsoft.Json.Tests/Converters/UnixDateTimeConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
#if DNXCORE50
using Xunit;
using Test = Xunit.FactAttribute;
Expand Down Expand Up @@ -71,6 +72,17 @@ public void SerializeInvalidDate()
);
}

[Test]
public void SerializeDateBeforeEpoch()
{
DateTime date = new DateTime(1964, 2, 7);
long dateSeconds = (long)(date.ToUniversalTime() - new DateTime(1970, 1, 1)).TotalSeconds;

string result = JsonConvert.SerializeObject(date, new UnixDateTimeConverter { AllowPreEpoch = true });

Assert.AreEqual(dateSeconds.ToString(CultureInfo.InvariantCulture), result);
}

[Test]
public void WriteJsonInvalidType()
{
Expand Down Expand Up @@ -159,6 +171,19 @@ public void DeserializeInvalidStringToDateTimeOffset()
"Cannot convert invalid value to System.DateTimeOffset. Path '', line 1, position 5."
);
}

[Test]
public void DeserializeDateTimeOffsetBeforeEpoch()
{
UnixDateTimeConverter converter = new UnixDateTimeConverter(true);
DateTimeOffset d = new DateTimeOffset(1969, 2, 1, 20, 6, 18, TimeSpan.Zero);

string json = JsonConvert.SerializeObject(d, converter);

DateTimeOffset result = JsonConvert.DeserializeObject<DateTimeOffset>(json, converter);

Assert.AreEqual(new DateTimeOffset(1969, 2, 1, 20, 6, 18, TimeSpan.Zero), result);
}
#endif

[Test]
Expand Down Expand Up @@ -186,6 +211,14 @@ public void DeserializeInvalidValue()
);
}

[Test]
public void DeserializeNegativeIntegerToDateTimeBeforeEpoch()
{
DateTime result = JsonConvert.DeserializeObject<DateTime>("-1514840476", new UnixDateTimeConverter(true));

Assert.AreEqual(new DateTime(1921, 12, 31, 02, 58, 44, DateTimeKind.Utc), result);
}

[Test]
public void DeserializeInvalidValueType()
{
Expand Down Expand Up @@ -263,6 +296,49 @@ public void ConverterObject()
Assert.IsNull(obj2.Object2);
Assert.AreEqual(new DateTime(2018, 1, 1, 21, 1, 16, DateTimeKind.Utc), obj2.ObjectNotHandled);
}

#if !NET20
[Test]
public void ConverterObjectWithDatesBeforeEpoch()
{
PreEpochUnixConverterObject obj1 = new PreEpochUnixConverterObject
{
Date1 = new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc),
Date2 = new DateTimeOffset(1969, 1, 1, 0, 0, 3, TimeSpan.Zero)
};

string json = JsonConvert.SerializeObject(obj1, Formatting.Indented);
StringAssert.AreEqual(@"{
""Date1"": -31535997,
""Date2"": -31535997
}", json);

PreEpochUnixConverterObject obj2 = JsonConvert.DeserializeObject<PreEpochUnixConverterObject>(json);
Assert.IsNotNull(obj2);

Assert.AreEqual(new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc), obj2.Date1);
Assert.AreEqual(new DateTimeOffset(1969, 1, 1, 0, 0, 3, TimeSpan.Zero), obj2.Date2);
}
#else
[Test]
public void ConverterObjectWithDatesBeforeEpoch()
{
PreEpochUnixConverterObject obj1 = new PreEpochUnixConverterObject
{
Date1 = new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc)
};

string json = JsonConvert.SerializeObject(obj1, Formatting.Indented);
StringAssert.AreEqual(@"{
""Date1"": -31535997
}", json);

PreEpochUnixConverterObject obj2 = JsonConvert.DeserializeObject<PreEpochUnixConverterObject>(json);
Assert.IsNotNull(obj2);

Assert.AreEqual(new DateTime(1969, 1, 1, 0, 0, 3, DateTimeKind.Utc), obj2.Date1);
}
#endif
}

[JsonArray(ItemConverterType = typeof(UnixDateTimeConverter))]
Expand All @@ -281,4 +357,15 @@ public class UnixConverterObject
[JsonConverter(typeof(UnixDateTimeConverter))]
public object ObjectNotHandled { get; set; }
}

public class PreEpochUnixConverterObject
{
[JsonConverter(typeof(UnixDateTimeConverter), true)]
public DateTime Date1 { get; set; }

#if !NET20
[JsonConverter (typeof(UnixDateTimeConverter), true)]
public DateTimeOffset Date2 { get; set; }
#endif
}
}
35 changes: 33 additions & 2 deletions Src/Newtonsoft.Json/Converters/UnixDateTimeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ public class UnixDateTimeConverter : DateTimeConverterBase
{
internal static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// Gets or sets a value indicating whether the dates before Unix epoch
/// should converted to and from JSON.
/// </summary>
/// <value>
/// <c>true</c> to allow converting dates before Unix epoch to and from JSON;
/// <c>false</c> to throw an exception when a date being converted to or from JSON
/// occurred before Unix epoch. The default value is <c>false</c>.
/// </value>
public bool AllowPreEpoch { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="UnixDateTimeConverter"/> class.
/// </summary>
public UnixDateTimeConverter() : this(false)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="UnixDateTimeConverter"/> class.
/// </summary>
/// <param name="allowPreEpoch">
/// <c>true</c> to allow converting dates before Unix epoch to and from JSON;
/// <c>false</c> to throw an exception when a date being converted to or from JSON
/// occurred before Unix epoch. The default value is <c>false</c>.
/// </param>
public UnixDateTimeConverter(bool allowPreEpoch)
{
AllowPreEpoch = allowPreEpoch;
}

/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
Expand All @@ -61,7 +92,7 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer
throw new JsonSerializationException("Expected date object value.");
}

if (seconds < 0)
if (!AllowPreEpoch && seconds < 0)
{
throw new JsonSerializationException("Cannot convert date value that is before Unix epoch of 00:00:00 UTC on 1 January 1970.");
}
Expand Down Expand Up @@ -108,7 +139,7 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer
throw JsonSerializationException.Create(reader, "Unexpected token parsing date. Expected Integer or String, got {0}.".FormatWith(CultureInfo.InvariantCulture, reader.TokenType));
}

if (seconds >= 0)
if (AllowPreEpoch || seconds >= 0)
{
DateTime d = UnixEpoch.AddSeconds(seconds);

Expand Down

0 comments on commit 7f78562

Please sign in to comment.