[C#] DateTime を指定した単位に切り上げ、切り捨て、四捨五入する
DateTime をミリ秒未満など指定した単位へ切り捨てや、10分単位など任意の単位へ丸める方法。
環境
- .NET 6.0.101
- C# 10.0
- Visual Studio 2022 Version 17.0.4
- ReSharper 2021.3.2
- Windows 10 Pro 64bit 21H1 19043.1415
実装
拡張メソッドでの実装
internal static class DateTimeExtensions
{
private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
public static DateTime RoundUpToDay(this DateTime value) => value.RoundUpCore(TimeSpan.TicksPerDay);
public static DateTime RoundUpToHour(this DateTime value) => value.RoundUpCore(TimeSpan.TicksPerHour);
public static DateTime RoundUpToMinute(this DateTime value) => value.RoundUpCore(TimeSpan.TicksPerMinute);
public static DateTime RoundUpToSecond(this DateTime value) => value.RoundUpCore(TimeSpan.TicksPerSecond);
public static DateTime RoundUpToMillisecond(this DateTime value) => value.RoundUpCore(TimeSpan.TicksPerMillisecond);
public static DateTime RoundUpToMicrosecond(this DateTime value) => value.RoundUpCore(TicksPerMicrosecond);
public static DateTime RoundUp(this DateTime value, TimeSpan interval) => value.RoundUpCore(interval.Ticks);
public static DateTime RoundDownToDay(this DateTime value) => value.RoundDownCore(TimeSpan.TicksPerDay);
public static DateTime RoundDownToHour(this DateTime value) => value.RoundDownCore(TimeSpan.TicksPerHour);
public static DateTime RoundDownToMinute(this DateTime value) => value.RoundDownCore(TimeSpan.TicksPerMinute);
public static DateTime RoundDownToSecond(this DateTime value) => value.RoundDownCore(TimeSpan.TicksPerSecond);
public static DateTime RoundDownToMillisecond(this DateTime value) => value.RoundDownCore(TimeSpan.TicksPerMillisecond);
public static DateTime RoundDownToMicrosecond(this DateTime value) => value.RoundDownCore(TicksPerMicrosecond);
public static DateTime RoundDown(this DateTime value, TimeSpan interval) => value.RoundDownCore(interval.Ticks);
public static DateTime RoundAwayFromZeroToDay(this DateTime value) => value.RoundAwayFromZeroCore(TimeSpan.TicksPerDay);
public static DateTime RoundAwayFromZeroToHour(this DateTime value) => value.RoundAwayFromZeroCore(TimeSpan.TicksPerHour);
public static DateTime RoundAwayFromZeroToMinute(this DateTime value) => value.RoundAwayFromZeroCore(TimeSpan.TicksPerMinute);
public static DateTime RoundAwayFromZeroToSecond(this DateTime value) => value.RoundAwayFromZeroCore(TimeSpan.TicksPerSecond);
public static DateTime RoundAwayFromZeroToMillisecond(this DateTime value) => value.RoundAwayFromZeroCore(TimeSpan.TicksPerMillisecond);
public static DateTime RoundAwayFromZeroToMicrosecond(this DateTime value) => value.RoundAwayFromZeroCore(TicksPerMicrosecond);
public static DateTime RoundAwayFromZero(this DateTime value, TimeSpan interval) => value.RoundAwayFromZeroCore(interval.Ticks);
private static DateTime RoundUpCore(this DateTime value, long interval)
{
return value.Ticks % interval is var overflow and > 0
? value.AddTicks(interval - overflow)
: value;
}
private static DateTime RoundDownCore(this DateTime value, long interval)
{
return value.AddTicks(-(value.Ticks % interval));
}
private static DateTime RoundAwayFromZeroCore(this DateTime value, long interval)
{
var ticks = value.Ticks + interval >> 1;
return new DateTime(ticks - ticks % interval, value.Kind);
}
}
使用方法
1. 切り上げ
// 切り上げた桁未満は 0 埋めになる
var d = new DateTime(2021, 11, 11, 11, 11, 11).AddTicks(1111111);
// 2021-11-11 11:11:11.1111111
d.RoundUpToDay(); // 2021-11-12 00:00:00.0000000
d.RoundUpToHour(); // 2021-11-11 12:00:00.0000000
d.RoundUpToMinute(); // 2021-11-11 11:12:00.0000000
d.RoundUpToSecond(); // 2021-11-11 11:11:12.0000000
d.RoundUpToMillisecond(); // 2021-11-11 11:11:11.1120000
d.RoundUpToMicrosecond(); // 2021-11-11 11:11:11.1111120
// AddTicks(1) (=100 ナノ秒) が含まれると必ず切り上げになる
var d = new DateTime(2021, 11, 11, 0, 0, 0).AddTicks(1);
// 2021-11-11 00:00:00.0000001
d.RoundUpToDay(); // 2021-11-12 00:00:00.0000000
d.RoundUpToHour(); // 2021-11-11 01:00:00.0000000
d.RoundUpToMinute(); // 2021-11-11 00:01:00.0000000
d.RoundUpToSecond(); // 2021-11-11 00:00:01.0000000
d.RoundUpToMillisecond(); // 2021-11-11 00:00:00.0010000
d.RoundUpToMicrosecond(); // 2021-11-11 00:00:00.0000010
2. 切り捨て
// 切り捨てた桁未満は 0 になる
var d = new DateTime(2021, 11, 11, 11, 11, 11).AddTicks(1111111);
// 2021-11-11 11:11:11.1111111
d.RoundDownToDay(); // 2021-11-11 00:00:00.0000000
d.RoundDownToHour(); // 2021-11-11 11:00:00.0000000
d.RoundDownToMinute(); // 2021-11-11 11:11:00.0000000
d.RoundDownToSecond(); // 2021-11-11 11:11:11.0000000
d.RoundDownToMillisecond(); // 2021-11-11 11:11:11.1110000
d.RoundDownToMicrosecond(); // 2021-11-11 11:11:11.1111110
3. 四捨五入
// 切り上げ
var d = new DateTime(2021, 11, 11, 12, 30, 30).AddTicks(500_500_5);
// 2021-11-11 12:30:30.5005005
d.RoundAwayFromZeroToDay(); // 2021-11-12 00:00:00.0000000
d.RoundAwayFromZeroToHour(); // 2021-11-11 13:00:00.0000000
d.RoundAwayFromZeroToMinute(); // 2021-11-11 12:31:00.0000000
d.RoundAwayFromZeroToSecond(); // 2021-11-11 12:30:31.0000000
d.RoundAwayFromZeroToMillisecond(); // 2021-11-11 12:30:30.5010000
d.RoundAwayFromZeroToMicrosecond(); // 2021-11-11 12:30:30.5005010
// 切り捨て
var d = new DateTime(2021, 11, 11, 11, 29, 29).AddTicks(499_499_4);
// 2021-11-11 11:29:29.4994994
d.RoundAwayFromZeroToDay(); // 2021-11-11 00:00:00.0000000
d.RoundAwayFromZeroToHour(); // 2021-11-11 11:00:00.0000000
d.RoundAwayFromZeroToMinute(); // 2021-11-11 11:29:00.0000000
d.RoundAwayFromZeroToSecond(); // 2021-11-11 11:29:29.0000000
d.RoundAwayFromZeroToMillisecond(); // 2021-11-11 11:29:29.4990000
d.RoundAwayFromZeroToMicrosecond(); // 2021-11-11 11:29:29.4994990
時分秒の四捨五入の境界値は以下の通り
桁 | 切り上げ | 切り捨て | 進数 |
---|---|---|---|
時 | 11 以下 | 12 以上 | 24 |
分 | 29 以下 | 30 以上 | 60 |
秒 | 29 以下 | 30 以上 | 60 |
秒未満 | 4 以下 | 5 以上 | 10 |
4. 任意の単位で丸める
TimeSpan で任意のインターバルを指定する。
// 0.01 秒単位に丸める
var interval = TimeSpan.FromMilliseconds(10);
var d = new DateTime(2021, 11, 11, 11, 11, 11).AddTicks(123_456_7);
// 2021-11-11 11:11:11.1234567
d.RoundDown(interval); // 2021-11-11 11:11:11.1200000
d.RoundUp(interval); // 2021-11-11 11:11:11.1300000
d.RoundAwayFromZero(interval); // 2021-11-11 11:11:11.1200000
// 10 分単位に丸める
var interval = TimeSpan.FromMinutes(10);
var d = new DateTime(2021, 11, 11, 0, 34, 59);
// 2021-11-11 00:34:59.0000000
d.RoundDown(interval); // 2021-11-11 00:30:00.0000000
d.RoundUp(interval); // 2021-11-11 00:40:00.0000000
d.RoundAwayFromZero(interval); // 2021-11-11 00:30:00.0000000
自分秒の進数で割り切れる値で指定しないと、期待した結果にならない可能性がある。
x は以下のような値を指定すると良い。
単位 | メソッド | x の値 |
---|---|---|
時 | TimeSpan.FromHours(x) | 2, 3, 4, 6 |
分 | TimeSpan.FromMinutes(x) | 2, 5, 10, 15, 20, 30 など |
秒 | TimeSpan.FromSeconds(x) | 2, 5, 10, 15, 20, 30 など |
秒未満 | TimeSpan.FromMilliseconds(x) | 5 以上 |
その他 | TimeSpan.FromDays(x) TimeSpan.FromTicks(x) | 基本使わない |
注意
1. Interval に TimeSpan.Zero を指定すると DivideByZeroException 例外が発生する
interval.Ticks == 0 の引数チェックの実装はしていないので、対応したければ修正が必要になる。
// DivideByZeroException が発生
new DateTime(2021, 11, 11).RoundDown(TimeSpan.FromMilliseconds(0);
2. DateTime.MaxValue を超える切り上げは ArgumentOutOfRangeException 例外が発生する
DateTime.MaxValue
を超える場合に DateTime.MaxValue
として返すようにしたければ、コード修正が必要になる。
対応するのであれば、DateTime.MaxTicks を超える場合に、DateTime.MaxValue
として返すようにすれば良いと思う。今回の例では対応しない。
// ArgumentOutOfRangeException が発生
new DateTime(9999,12,31,0,0,1).RoundUpToDay();
3. C# 8.0 以前では実装に修正が必要
RoundUpCore()
は C# 9.0 のパターンマッチング を使用しているため、C# 8.0 以前の場合は修正が必要になる。
private static DateTime RoundUpCore(this DateTime value, long interval)
{
// return value.Ticks % interval is var overflow and > 0
// ? value.AddTicks(interval - overflow)
// : value;
// C# 8.0 以前に対応
var overflow = value.Ticks % interval;
return overflow == 0 ? value : value.AddTicks(interval - overflow);
}
上記の overflow == 0
では、ReSharper 2021.3.2 で Code Inspection: Expression is always ’true’ or always ‘false’ が表示されたので、パターンマッチングに修正した。
ちなみに、overflow.Equals(0)
だと問題ない。
Rider でも同様なので、ReSharper に Feedback は送ってみた。
実装の悩み
1. RoundUp, RoundDown というメソッド名
Math.Ceiling()
と Math.Floor()
にメソッド名を合わせると本当は、
- RoundUp -> Ceiling
- RoundDown -> Floor
にすべきかなと思う。
だけど、C#慣れしていない人や IntelliSense による入力補完を考えると Round に統一した方が分かりやすいと思ったので、RoundUp, RoundDown にした。
2. RoundAwayFromZero というメソッド名
名前が長くなってしまうが Round
ではなく RoundAwayFromZero
というメソッド名にした。
これは Math.Round()
の四捨五入がデフォルトでは MidpointRounding.ToEven
(銀行丸め) であり、MidpointRounding.RoundAwayFromZero
であることを明確にするため、メソッド名に付けた。
3. その他の実装方法とパフォーマンス
他にも実装方法はいくつかあるので調べてみる。
切り上げ
public static DateTime RoundUp1(this DateTime value, TimeSpan interval)
{
return value.Ticks % interval.Ticks is var overflow and > 0
? value.AddTicks(interval.Ticks - overflow)
: value;
}
public static DateTime RoundUp2(this DateTime value, TimeSpan interval)
{
return new DateTime((value.Ticks + interval.Ticks - 1) / interval.Ticks * interval.Ticks, value.Kind);
}
切り捨て
public static DateTime RoundDown1(this DateTime value, TimeSpan interval)
{
return value.AddTicks(-(value.Ticks % interval.Ticks));
}
public static DateTime RoundDown2(this DateTime value, TimeSpan interval)
{
return new DateTime(((value.Ticks + interval.Ticks) / interval.Ticks - 1) * interval.Ticks, value.Kind);
}
public static DateTime RoundDown3(this DateTime value, TimeSpan interval)
{
var ticks = value.Ticks / interval.Ticks;
return new DateTime(ticks * interval.Ticks, value.Kind);
}
四捨五入
public static DateTime Round1(this DateTime value, TimeSpan interval)
{
var halfIntervalTicks = interval.Ticks >> 1;
return value.AddTicks(halfIntervalTicks - (value.Ticks + halfIntervalTicks) % interval.Ticks);
}
public static DateTime Round2(this DateTime value, TimeSpan interval)
{
var ticks = (value.Ticks + interval.Ticks >> 1) / interval.Ticks;
return new DateTime(ticks * interval.Ticks, value.Kind);
}
public static DateTime Round3(this DateTime value, TimeSpan interval)
{
var ticks = value.Ticks + interval.Ticks >> 1;
return new DateTime(ticks - ticks % interval.Ticks, value.Kind);
}
性能も何回か計測してみた上で
- RoundUp は RoundUp1 が少し高速。
- RoundDown は性能誤差なので、実装は RoundDown1 がシンプル。
- Round は性能誤差だけど、Round3 が少し高速な傾向。
interval.Ticks / 2
はinterval.Ticks >> 1
の方が少し高速。- new DateTime() コンストラクタで ticks を使わない方法では、約20倍以上遅い。
- new DateTime() コンストラクタだと、引数に Kind を指定 し忘れないように注意が必要。(DateTimeKind.Unspecified になるため)
- new DateTime() コンストラクタだと kind の引数判定が入るが、逆に AddTicks() だと Ticks + value が入る。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1415 (21H1/May2021Update)
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK=6.0.101
[Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
.NET 6.0 : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
Job=.NET 6.0 Runtime=.NET 6.0
Method | value | interval | Mean | Error | StdDev | Ratio | RatioSD |
---|---|---|---|---|---|---|---|
RoundUp1 | 11/11/2021 11:11:11 | 1.00:00:00 | 4.119 ns | 0.0697 ns | 0.0652 ns | 1.00 | 0.00 |
RoundUp2 | 11/11/2021 11:11:11 | 1.00:00:00 | 4.183 ns | 0.0379 ns | 0.0336 ns | 1.01 | 0.01 |
RoundDown1 | 11/11/2021 11:11:11 | 1.00:00:00 | 4.085 ns | 0.0431 ns | 0.0403 ns | 1.00 | 0.00 |
RoundDown2 | 11/11/2021 11:11:11 | 1.00:00:00 | 4.067 ns | 0.0200 ns | 0.0187 ns | 1.00 | 0.01 |
RoundDown3 | 11/11/2021 11:11:11 | 1.00:00:00 | 4.039 ns | 0.0450 ns | 0.0421 ns | 0.99 | 0.02 |
Round1 | 11/11/2021 11:11:11 | 1.00:00:00 | 4.051 ns | 0.0127 ns | 0.0106 ns | 1.00 | 0.00 |
Round2 | 11/11/2021 11:11:11 | 1.00:00:00 | 3.839 ns | 0.0229 ns | 0.0214 ns | 0.95 | 0.00 |
Round3 | 11/11/2021 11:11:11* | 1.00:00:00 | 3.817 ns | 0.0274 ns | 0.0242 ns | 0.94 | 0.01 |
RoundUp1 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 6.999 ns | 0.0326 ns | 0.0305 ns | 1.00 | 0.00 |
RoundUp2 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 7.034 ns | 0.0392 ns | 0.0367 ns | 1.01 | 0.01 |
RoundDown1 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 7.088 ns | 0.0769 ns | 0.0682 ns | 1.00 | 0.00 |
RoundDown2 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 7.095 ns | 0.1035 ns | 0.0968 ns | 1.00 | 0.02 |
RoundDown3 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 7.149 ns | 0.0921 ns | 0.0861 ns | 1.01 | 0.02 |
Round1 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 7.200 ns | 0.0920 ns | 0.0861 ns | 1.00 | 0.00 |
Round2 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 7.011 ns | 0.1045 ns | 0.0977 ns | 0.97 | 0.01 |
Round3 | 12/31/2021 23:59:59 | 00:00:00.0010000 | 6.771 ns | 0.0027 ns | 0.0021 ns | 0.94 | 0.01 |
Benchmarkコード例
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<DateTimeRoundBenchmark>();
[SimpleJob(RuntimeMoniker.Net60)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public class DateTimeRoundBenchmark
{
public IEnumerable<object[]> DateTimes()
{
yield return new object[] { new DateTime(2021, 12, 31, 23, 59, 59).AddTicks(999_999_9), new TimeSpan(TimeSpan.TicksPerMillisecond) };
yield return new object[] { new DateTime(2021, 11, 11, 11, 11, 11).AddTicks(111_111_1), new TimeSpan(TimeSpan.TicksPerDay) };
}
// RoundUp
[Benchmark(Baseline = true)]
[BenchmarkCategory("RoundUp")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime RoundUp1(DateTime value, TimeSpan interval)
{
return value.RoundUp1(interval);
}
[Benchmark]
[BenchmarkCategory("RoundUp")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime RoundUp2(DateTime value, TimeSpan interval)
{
return value.RoundUp2(interval);
}
// RoundDown
[Benchmark(Baseline = true)]
[BenchmarkCategory("RoundDown")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime RoundDown1(DateTime value, TimeSpan interval)
{
return value.RoundDown1(interval);
}
[Benchmark]
[BenchmarkCategory("RoundDown")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime RoundDown2(DateTime value, TimeSpan interval)
{
return value.RoundDown2(interval);
}
[Benchmark]
[BenchmarkCategory("RoundDown")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime RoundDown3(DateTime value, TimeSpan interval)
{
return value.RoundDown3(interval);
}
// Round
[Benchmark(Baseline = true)]
[BenchmarkCategory("Round")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime Round1(DateTime value, TimeSpan interval)
{
return value.Round1(interval);
}
[Benchmark]
[BenchmarkCategory("Round")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime Round2(DateTime value, TimeSpan interval)
{
return value.Round2(interval);
}
[Benchmark]
[BenchmarkCategory("Round")]
[ArgumentsSource(nameof(DateTimes))]
public DateTime Round3(DateTime value, TimeSpan interval)
{
return value.Round3(interval);
}
}
internal static class DateTimeExtensions
{
// RoundUp
public static DateTime RoundUp1(this DateTime value, TimeSpan interval)
{
return value.Ticks % interval.Ticks is var overflow and > 0
? value.AddTicks(interval.Ticks - overflow)
: value;
}
public static DateTime RoundUp2(this DateTime value, TimeSpan interval)
{
return new DateTime((value.Ticks + interval.Ticks - 1) / interval.Ticks * interval.Ticks, value.Kind);
}
// RoundDown
public static DateTime RoundDown1(this DateTime value, TimeSpan interval)
{
return value.AddTicks(-(value.Ticks % interval.Ticks));
}
public static DateTime RoundDown2(this DateTime value, TimeSpan interval)
{
return new DateTime(((value.Ticks + interval.Ticks) / interval.Ticks - 1) * interval.Ticks, value.Kind);
}
public static DateTime RoundDown3(this DateTime value, TimeSpan interval)
{
var ticks = value.Ticks / interval.Ticks;
return new DateTime(ticks * interval.Ticks, value.Kind);
}
// Round
public static DateTime Round1(this DateTime value, TimeSpan interval)
{
var halfIntervalTicks = interval.Ticks >> 1;
return value.AddTicks(halfIntervalTicks - (value.Ticks + halfIntervalTicks) % interval.Ticks);
}
public static DateTime Round2(this DateTime value, TimeSpan interval)
{
var ticks = (value.Ticks + interval.Ticks >> 1) / interval.Ticks;
return new DateTime(ticks * interval.Ticks, value.Kind);
}
public static DateTime Round3(this DateTime value, TimeSpan interval)
{
var ticks = value.Ticks + interval.Ticks >> 1;
return new DateTime(ticks - ticks % interval.Ticks, value.Kind);
}
}
感謝
- TimeSpan.Ticks を使用したRound
- TimeSpan.Ticks を使用した切り捨て
- TimeSpan.TicksPerXxx を使用した切り捨て
- DateTime コンストラクタを使用した切り捨て
関連記事
新着記事