-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement DateOnly and TimeOnly (#1100)
* implement DateOnly and TimeOnly fix #977 * use raw TimeOnly.Ticks * release notes
- Loading branch information
Showing
15 changed files
with
385 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
src/protobuf-net.Core/Internal/PrimaryTypeProvider.DateTimeOnly.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
using ProtoBuf.Serializers; | ||
using System; | ||
|
||
#if NET6_0_OR_GREATER | ||
namespace ProtoBuf.Internal | ||
{ | ||
// map DateOnly as "int32" (days since January 1, 0001 in the Proleptic Gregorian calendar), | ||
// and TimeOnly as "int64" (ticks into day, where a tick is 100ns) | ||
// | ||
// it was tempting to map to Date and TimeOfDay respectively, but this has problems: | ||
// - Date allows dates a date without a year to be expressed, which DateOnly does not | ||
// - TimeOfDay allows 24:00 to be expressed, which TimeOnly does not | ||
// likewise, there is Timestamp. but that is ... awkward and heavy for pure dates, | ||
// and Duration has larger range which will explode TimeOnly - it would be artificial | ||
// to pretend that they can interop with either of these | ||
// | ||
// either way, there's also a minor precision issue, since the google types go to | ||
// nanosecond precision, and TimeOfDay only allows ticks (100ns), but in reality this | ||
// is unlikely to be a problem; anyone requiring better accuracy should probably handle | ||
// it manually, or (simpler) use WellKnownTypes.Duration / WellKnownTypes.Timestamp directly | ||
// | ||
// refs: | ||
// https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/timestamp.proto | ||
// https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/duration.proto | ||
// https://github.com/googleapis/googleapis/blob/master/google/type/timeofday.proto | ||
// https://github.com/googleapis/googleapis/blob/master/google/type/date.proto | ||
|
||
partial class PrimaryTypeProvider : | ||
ISerializer<DateOnly>, ISerializer<DateOnly?>, | ||
IValueChecker<DateOnly>, IValueChecker<DateOnly?>, | ||
IValueChecker<TimeOnly>, IValueChecker<TimeOnly?>, | ||
IMeasuringSerializer<DateOnly>, IMeasuringSerializer<DateOnly?>, | ||
IMeasuringSerializer<TimeOnly>, IMeasuringSerializer<TimeOnly?>, | ||
|
||
ISerializer<TimeOnly>, ISerializer<TimeOnly?> | ||
{ | ||
SerializerFeatures ISerializer<DateOnly>.Features => SerializerFeatures.WireTypeVarint | SerializerFeatures.CategoryScalar; | ||
SerializerFeatures ISerializer<DateOnly?>.Features => SerializerFeatures.WireTypeVarint | SerializerFeatures.CategoryScalar; | ||
|
||
SerializerFeatures ISerializer<TimeOnly>.Features => SerializerFeatures.WireTypeVarint | SerializerFeatures.CategoryScalar; | ||
SerializerFeatures ISerializer<TimeOnly?>.Features => SerializerFeatures.WireTypeVarint | SerializerFeatures.CategoryScalar; | ||
|
||
TimeOnly ISerializer<TimeOnly>.Read(ref ProtoReader.State state, TimeOnly value) | ||
=> new TimeOnly(state.ReadInt64()); | ||
|
||
void ISerializer<TimeOnly>.Write(ref ProtoWriter.State state, TimeOnly value) | ||
=> state.WriteInt64(value.Ticks); | ||
|
||
TimeOnly? ISerializer<TimeOnly?>.Read(ref ProtoReader.State state, TimeOnly? value) | ||
=> new TimeOnly(state.ReadInt64()); | ||
|
||
void ISerializer<TimeOnly?>.Write(ref ProtoWriter.State state, TimeOnly? value) | ||
=> state.WriteInt64(value.GetValueOrDefault().Ticks); | ||
|
||
DateOnly ISerializer<DateOnly>.Read(ref ProtoReader.State state, DateOnly value) | ||
=> DateOnly.FromDayNumber(state.ReadInt32()); | ||
|
||
void ISerializer<DateOnly>.Write(ref ProtoWriter.State state, DateOnly value) | ||
=> state.WriteInt32(value.DayNumber); | ||
|
||
DateOnly? ISerializer<DateOnly?>.Read(ref ProtoReader.State state, DateOnly? value) | ||
=> DateOnly.FromDayNumber(state.ReadInt32()); | ||
|
||
void ISerializer<DateOnly?>.Write(ref ProtoWriter.State state, DateOnly? value) | ||
=> state.WriteInt32(value.GetValueOrDefault().DayNumber); | ||
|
||
|
||
bool IValueChecker<DateOnly>.HasNonTrivialValue(DateOnly value) => value.DayNumber != 0; | ||
bool IValueChecker<DateOnly>.IsNull(DateOnly value) => false; | ||
|
||
bool IValueChecker<DateOnly?>.HasNonTrivialValue(DateOnly? value) => value.GetValueOrDefault().DayNumber != 0; | ||
bool IValueChecker<DateOnly?>.IsNull(DateOnly? value) => value is null; | ||
int IMeasuringSerializer<DateOnly>.Measure(ISerializationContext context, WireType wireType, DateOnly value) | ||
=> ProtoWriter.MeasureInt32(value.DayNumber); | ||
|
||
int IMeasuringSerializer<DateOnly?>.Measure(ISerializationContext context, WireType wireType, DateOnly? value) | ||
=> ProtoWriter.MeasureInt32(value.Value.DayNumber); | ||
|
||
bool IValueChecker<TimeOnly>.HasNonTrivialValue(TimeOnly value) => value.Ticks != 0; | ||
bool IValueChecker<TimeOnly>.IsNull(TimeOnly value) => false; | ||
|
||
bool IValueChecker<TimeOnly?>.HasNonTrivialValue(TimeOnly? value) => value.GetValueOrDefault().Ticks != 0; | ||
bool IValueChecker<TimeOnly?>.IsNull(TimeOnly? value) => value is null; | ||
int IMeasuringSerializer<TimeOnly>.Measure(ISerializationContext context, WireType wireType, TimeOnly value) | ||
=> ProtoWriter.MeasureInt64(value.Ticks); | ||
|
||
int IMeasuringSerializer<TimeOnly?>.Measure(ISerializationContext context, WireType wireType, TimeOnly? value) | ||
=> ProtoWriter.MeasureInt64(value.Value.Ticks); | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
using ProtoBuf.Meta; | ||
using ProtoBuf.unittest; | ||
using System; | ||
using System.IO; | ||
using Xunit; | ||
|
||
#if NET6_0_OR_GREATER | ||
namespace ProtoBuf.Test | ||
{ | ||
public class DateTimeOnlyTests | ||
{ | ||
[Fact] | ||
public void SchemaFlat() | ||
{ | ||
var model = RuntimeTypeModel.Create(); | ||
Assert.Equal(@"syntax = ""proto3""; | ||
package ProtoBuf.Test; | ||
message SomeType { | ||
int32 Date = 1; | ||
int64 Time = 2; | ||
} | ||
", model.GetSchema(typeof(SomeType)), ignoreLineEndingDifferences: true); | ||
} | ||
|
||
[Fact] | ||
public void SchemaArrays() | ||
{ | ||
var model = RuntimeTypeModel.Create(); | ||
Assert.Equal(@"syntax = ""proto3""; | ||
package ProtoBuf.Test; | ||
message SomeTypeWithArrays { | ||
repeated int32 Dates = 1; | ||
repeated int64 Times = 2; | ||
} | ||
", model.GetSchema(typeof(SomeTypeWithArrays)), ignoreLineEndingDifferences: true); | ||
} | ||
|
||
[Fact] | ||
public void CanRoundtripSimpleAndArrays() | ||
{ | ||
var model = RuntimeTypeModel.Create(); | ||
model.AutoCompile = false; | ||
model.Add<SomeType>(); | ||
model.Add<SomeTypeWithArrays>(); | ||
|
||
ExecuteSimple(model); | ||
ExecuteArrays(model); | ||
|
||
model.CompileInPlace(); | ||
ExecuteSimple(model); | ||
ExecuteArrays(model); | ||
|
||
var compiled = model.Compile(); | ||
ExecuteSimple(compiled); | ||
ExecuteArrays(compiled); | ||
|
||
compiled = PEVerify.CompileAndVerify(model); | ||
ExecuteSimple(compiled); | ||
ExecuteArrays(compiled); | ||
} | ||
|
||
void ExecuteSimple(TypeModel model) | ||
{ | ||
var obj = new SomeType { | ||
Date = new DateOnly(2023, 09, 27), | ||
Time = new TimeOnly(11, 55, 32, 904), | ||
}; | ||
|
||
using var ms = new MemoryStream(); | ||
model.Serialize(ms, obj); | ||
var hex = BitConverter.ToString(ms.GetBuffer(), 0, (int)ms.Length); | ||
Assert.Equal("08-E5-8B-2D-10-80-85-85-B0-BF-0C", hex); | ||
/* | ||
Field #1: 08 Varint Value = 738789, Hex = E5-8B-2D | ||
Field #2: 10 Varint Value = 429329040000, Hex = 80-85-85-B0-BF-0C | ||
*/ | ||
|
||
|
||
ms.Position = 0; | ||
var clone = model.Deserialize<SomeType>(ms); | ||
Assert.NotNull(clone); | ||
Assert.NotSame(obj, clone); | ||
Assert.Equal(obj.Date, clone.Date); | ||
Assert.Equal(obj.Time, clone.Time); | ||
} | ||
|
||
void ExecuteArrays(TypeModel model) | ||
{ | ||
var obj = new SomeTypeWithArrays | ||
{ | ||
Dates = new[] { new DateOnly(2023, 09, 27) }, | ||
Times = new[] { new TimeOnly(11, 55, 32, 904) }, | ||
}; | ||
|
||
using var ms = new MemoryStream(); | ||
model.Serialize(ms, obj); | ||
var hex = BitConverter.ToString(ms.GetBuffer(), 0, (int)ms.Length); | ||
Assert.Equal("08-E5-8B-2D-10-80-85-85-B0-BF-0C", hex); | ||
// same payload | ||
|
||
ms.Position = 0; | ||
var clone = model.Deserialize<SomeTypeWithArrays>(ms); | ||
Assert.NotNull(clone); | ||
Assert.NotSame(obj, clone); | ||
Assert.Equal(obj.Dates, clone.Dates); | ||
Assert.Equal(obj.Times, clone.Times); | ||
} | ||
|
||
[ProtoContract] | ||
public class SomeType | ||
{ | ||
[ProtoMember(1)] | ||
public DateOnly Date { get; set; } | ||
|
||
[ProtoMember(2)] | ||
public TimeOnly Time { get; set; } | ||
} | ||
|
||
[ProtoContract] | ||
public class SomeTypeWithArrays | ||
{ | ||
[ProtoMember(1)] | ||
public DateOnly[] Dates { get; set; } | ||
|
||
[ProtoMember(2)] | ||
public TimeOnly[] Times { get; set; } | ||
} | ||
} | ||
} | ||
|
||
#endif |
Oops, something went wrong.