aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/clickhouse/src/Common/DateLUTImpl.h
blob: 6d0ba7180578db229eb820ebe1ee94635a623dd1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
#pragma once

#include <base/DayNum.h>
#include <base/defines.h>
#include <base/types.h>

#include <ctime>
#include <cassert>
#include <string>
#include <type_traits>


#define DATE_SECONDS_PER_DAY 86400 /// Number of seconds in a day, 60 * 60 * 24

#define DATE_LUT_MIN_YEAR 1900 /// 1900 since majority of financial organizations consider 1900 as an initial year.
#define DATE_LUT_MAX_YEAR 2299 /// Last supported year (complete)
#define DATE_LUT_YEARS (1 + DATE_LUT_MAX_YEAR - DATE_LUT_MIN_YEAR) /// Number of years in lookup table

#define DATE_LUT_SIZE 0x23AB1

#define DATE_LUT_MAX (0xFFFFFFFFU - 86400)
#define DATE_LUT_MAX_DAY_NUM 0xFFFF

#define DAYNUM_OFFSET_EPOCH 25567

/// Max int value of Date32, DATE LUT cache size minus daynum_offset_epoch
#define DATE_LUT_MAX_EXTEND_DAY_NUM (DATE_LUT_SIZE - DAYNUM_OFFSET_EPOCH)

/// A constant to add to time_t so every supported time point becomes non-negative and still has the same remainder of division by 3600.
/// If we treat "remainder of division" operation in the sense of modular arithmetic (not like in C++).
#define DATE_LUT_ADD ((1970 - DATE_LUT_MIN_YEAR) * 366L * 86400)


/// Flags for toYearWeek() function.
enum class WeekModeFlag : UInt8
{
    MONDAY_FIRST = 1,
    YEAR = 2,
    FIRST_WEEKDAY = 4,
    NEWYEAR_DAY = 8
};
using YearWeek = std::pair<UInt16, UInt8>;

/// Modes for toDayOfWeek() function.
enum class WeekDayMode
{
    WeekStartsMonday1 = 0,
    WeekStartsMonday0 = 1,
    WeekStartsSunday0 = 2,
    WeekStartsSunday1 = 3
};

/** Lookup table to conversion of time to date, and to month / year / day of week / day of month and so on.
  * First time was implemented for OLAPServer, that needed to do billions of such transformations.
  */
class DateLUTImpl
{
private:
    friend class DateLUT;
    explicit DateLUTImpl(const std::string & time_zone);

    DateLUTImpl(const DateLUTImpl &) = delete; /// NOLINT
    DateLUTImpl & operator=(const DateLUTImpl &) = delete; /// NOLINT
    DateLUTImpl(const DateLUTImpl &&) = delete; /// NOLINT
    DateLUTImpl & operator=(const DateLUTImpl &&) = delete; /// NOLINT

    // Normalized and bound-checked index of element in lut,
    // has to be a separate type to support overloading
    // TODO: make sure that any arithmetic on LUTIndex actually results in valid LUTIndex.
    STRONG_TYPEDEF(UInt32, LUTIndex)
    // Same as above but select different function overloads for zero saturation.
    STRONG_TYPEDEF(UInt32, LUTIndexWithSaturation)

    static inline LUTIndex normalizeLUTIndex(UInt32 index)
    {
        if (index >= DATE_LUT_SIZE)
            return LUTIndex(DATE_LUT_SIZE - 1);
        return LUTIndex{index};
    }

    static inline LUTIndex normalizeLUTIndex(Int64 index)
    {
        if (unlikely(index < 0))
            return LUTIndex(0);
        if (index >= DATE_LUT_SIZE)
            return LUTIndex(DATE_LUT_SIZE - 1);
        return LUTIndex{static_cast<UInt32>(index)};
    }

    template <typename T>
    friend inline LUTIndex operator+(const LUTIndex & index, const T v)
    {
        return normalizeLUTIndex(index.toUnderType() + UInt32(v));
    }

    template <typename T>
    friend inline LUTIndex operator+(const T v, const LUTIndex & index)
    {
        return normalizeLUTIndex(static_cast<Int64>(v + index.toUnderType()));
    }

    friend inline LUTIndex operator+(const LUTIndex & index, const LUTIndex & v)
    {
        return normalizeLUTIndex(static_cast<UInt32>(index.toUnderType() + v.toUnderType()));
    }

    template <typename T>
    friend inline LUTIndex operator-(const LUTIndex & index, const T v)
    {
        return normalizeLUTIndex(static_cast<Int64>(index.toUnderType() - UInt32(v)));
    }

    template <typename T>
    friend inline LUTIndex operator-(const T v, const LUTIndex & index)
    {
        return normalizeLUTIndex(static_cast<Int64>(v - index.toUnderType()));
    }

    friend inline LUTIndex operator-(const LUTIndex & index, const LUTIndex & v)
    {
        return normalizeLUTIndex(static_cast<Int64>(index.toUnderType() - v.toUnderType()));
    }

    template <typename T>
    friend inline LUTIndex operator*(const LUTIndex & index, const T v)
    {
        return normalizeLUTIndex(index.toUnderType() * UInt32(v));
    }

    template <typename T>
    friend inline LUTIndex operator*(const T v, const LUTIndex & index)
    {
        return normalizeLUTIndex(v * index.toUnderType());
    }

    template <typename T>
    friend inline LUTIndex operator/(const LUTIndex & index, const T v)
    {
        return normalizeLUTIndex(index.toUnderType() / UInt32(v));
    }

    template <typename T>
    friend inline LUTIndex operator/(const T v, const LUTIndex & index)
    {
        return normalizeLUTIndex(UInt32(v) / index.toUnderType());
    }

public:
    /// We use Int64 instead of time_t because time_t is mapped to the different types (long or long long)
    /// on Linux and Darwin (on both of them, long and long long are 64 bit and behaves identically,
    /// but they are different types in C++ and this affects function overload resolution).
    using Time = Int64;

    /// The order of fields matters for alignment and sizeof.
    struct Values
    {
        /// Time at beginning of the day.
        Time date;

        /// Properties of the day.
        UInt16 year;
        UInt8 month;
        UInt8 day_of_month;
        UInt8 day_of_week;

        /// Total number of days in current month. Actually we can use separate table that is independent of time zone.
        /// But due to alignment, this field is totally zero cost.
        UInt8 days_in_month;

        /// For days, when offset from UTC was changed due to daylight saving time or permanent change, following values could be non zero.
        /// All in OffsetChangeFactor (15 minute) intervals.
        Int8 amount_of_offset_change_value; /// Usually -4 or 4, but look at Lord Howe Island. Multiply by OffsetChangeFactor
        UInt8 time_at_offset_change_value; /// In seconds from beginning of the day. Multiply by OffsetChangeFactor

        inline Int32 amount_of_offset_change() const /// NOLINT
        {
            return static_cast<Int32>(amount_of_offset_change_value) * OffsetChangeFactor;
        }

        inline UInt32 time_at_offset_change() const /// NOLINT
        {
            return static_cast<UInt32>(time_at_offset_change_value) * OffsetChangeFactor;
        }

        /// Since most of the modern timezones have a DST change aligned to 15 minutes, to save as much space as possible inside Value,
        /// we are dividing any offset change related value by this factor before setting it to Value,
        /// hence it has to be explicitly multiplied back by this factor before being used.
        static constexpr UInt16 OffsetChangeFactor = 900;
    };

    static_assert(sizeof(Values) == 16);

private:
    /// Offset to epoch in days (ExtendedDayNum) of the first day in LUT.
    /// "epoch" is the Unix Epoch (starts at unix timestamp zero)
    static constexpr UInt32 daynum_offset_epoch = 25567;
    static_assert(daynum_offset_epoch == (1970 - DATE_LUT_MIN_YEAR) * 365 + (1970 - DATE_LUT_MIN_YEAR / 4 * 4) / 4);

    /// Lookup table is indexed by LUTIndex.
    /// Day nums are the same in all time zones. 1970-01-01 is 0 and so on.
    /// Table is relatively large, so better not to place the object on stack.
    /// In comparison to std::vector, plain array is cheaper by one indirection.
    Values lut[DATE_LUT_SIZE + 1];

    /// Same as above but with dates < 1970-01-01 saturated to 1970-01-01.
    Values lut_saturated[DATE_LUT_SIZE + 1];

    /// Year number after DATE_LUT_MIN_YEAR -> LUTIndex in lut for start of year.
    LUTIndex years_lut[DATE_LUT_YEARS];

    /// Year number after DATE_LUT_MIN_YEAR * month number starting at zero -> day num for first day of month
    LUTIndex years_months_lut[DATE_LUT_YEARS * 12];

    /// UTC offset at beginning of the Unix epoch. The same as unix timestamp of 1970-01-01 00:00:00 local time.
    Time offset_at_start_of_epoch;
    /// UTC offset at the beginning of the first supported year.
    Time offset_at_start_of_lut;
    bool offset_is_whole_number_of_hours_during_epoch;
    bool offset_is_whole_number_of_minutes_during_epoch;

    /// Time zone name.
    std::string time_zone;

    inline LUTIndex findIndex(Time t) const
    {
        /// First guess.
        Time guess = (t / 86400) + daynum_offset_epoch;

        /// For negative Time the integer division was rounded up, so the guess is offset by one.
        if (unlikely(t < 0))
            --guess;

        if (guess < 0)
            return LUTIndex(0);
        if (guess >= DATE_LUT_SIZE)
            return LUTIndex(DATE_LUT_SIZE - 1);

        /// UTC offset is from -12 to +14 in all known time zones. This requires checking only three indices.

        if (t >= lut[guess].date)
        {
            if (guess + 1 >= DATE_LUT_SIZE || t < lut[guess + 1].date)
                return LUTIndex(static_cast<unsigned>(guess));

            return LUTIndex(static_cast<unsigned>(guess) + 1);
        }

        return LUTIndex(guess ? static_cast<unsigned>(guess) - 1 : 0);
    }

    static inline LUTIndex toLUTIndex(DayNum d)
    {
        return normalizeLUTIndex(d + daynum_offset_epoch);
    }

    static inline LUTIndex toLUTIndex(ExtendedDayNum d)
    {
        return normalizeLUTIndex(static_cast<Int64>(d + daynum_offset_epoch));
    }

    inline LUTIndex toLUTIndex(Time t) const
    {
        return findIndex(t);
    }

    static inline LUTIndex toLUTIndex(LUTIndex i)
    {
        return i;
    }

    template <typename DateOrTime>
    inline const Values & find(DateOrTime v) const
    {
        return lut[toLUTIndex(v)];
    }

    template <typename DateOrTime, typename Divisor>
    inline DateOrTime roundDown(DateOrTime x, Divisor divisor) const
    {
        static_assert(std::is_integral_v<DateOrTime> && std::is_integral_v<Divisor>);
        assert(divisor > 0);

        if (likely(offset_is_whole_number_of_hours_during_epoch))
        {
            if (likely(x >= 0))
                return static_cast<DateOrTime>(x / divisor * divisor);

            /// Integer division for negative numbers rounds them towards zero (up).
            /// We will shift the number so it will be rounded towards -inf (down).
            return static_cast<DateOrTime>((x + 1 - divisor) / divisor * divisor);
        }

        Time date = find(x).date;
        Time res = date + (x - date) / divisor * divisor;
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
        {
            if (unlikely(res < 0))
                return 0;
            return static_cast<DateOrTime>(res);
        }
        else
            return res;
    }

public:
    const std::string & getTimeZone() const { return time_zone; }

    // Methods only for unit-testing, it makes very little sense to use it from user code.
    auto getOffsetAtStartOfEpoch() const { return offset_at_start_of_epoch; }
    auto getTimeOffsetAtStartOfLUT() const { return offset_at_start_of_lut; }

    static auto getDayNumOffsetEpoch()  { return daynum_offset_epoch; }

    /// All functions below are thread-safe; arguments are not checked.

    static UInt32 saturateMinus(UInt32 x, UInt32 y)
    {
        UInt32 res = x - y;
        res &= -Int32(res <= x);
        return res;
    }

    static ExtendedDayNum toDayNum(ExtendedDayNum d)
    {
        return d;
    }

    static ExtendedDayNum toDayNum(LUTIndex d)
    {
        return ExtendedDayNum{static_cast<ExtendedDayNum::UnderlyingType>(d.toUnderType() - daynum_offset_epoch)};
    }

    static DayNum toDayNum(LUTIndexWithSaturation d)
    {
        return DayNum{static_cast<DayNum::UnderlyingType>(saturateMinus(d.toUnderType(), daynum_offset_epoch))};
    }

    template <typename DateOrTime>
    inline auto toDayNum(DateOrTime v) const
    {
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return DayNum{static_cast<DayNum::UnderlyingType>(saturateMinus(toLUTIndex(v).toUnderType(), daynum_offset_epoch))};
        else
            return ExtendedDayNum{static_cast<ExtendedDayNum::UnderlyingType>(toLUTIndex(v).toUnderType() - daynum_offset_epoch)};
    }

    /// Round down to start of monday.
    template <typename DateOrTime>
    inline Time toFirstDayOfWeek(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return lut_saturated[i - (lut[i].day_of_week - 1)].date;
        else
            return lut[i - (lut[i].day_of_week - 1)].date;
    }

    template <typename DateOrTime>
    inline auto toFirstDayNumOfWeek(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(i - (lut[i].day_of_week - 1)));
        else
            return toDayNum(LUTIndex(i - (lut[i].day_of_week - 1)));
    }

    /// Round up to the last day of week.
    template <typename DateOrTime>
    inline Time toLastDayOfWeek(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return lut_saturated[i + (7 - lut[i].day_of_week)].date;
        else
            return lut[i + (7 - lut[i].day_of_week)].date;
    }

    template <typename DateOrTime>
    inline auto toLastDayNumOfWeek(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(i + (7 - lut[i].day_of_week)));
        else
            return toDayNum(LUTIndex(i + (7 - lut[i].day_of_week)));
    }

    /// Round down to start of month.
    template <typename DateOrTime>
    inline Time toFirstDayOfMonth(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return lut_saturated[i - (lut[i].day_of_month - 1)].date;
        else
            return lut[i - (lut[i].day_of_month - 1)].date;
    }

    template <typename DateOrTime>
    inline auto toFirstDayNumOfMonth(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(i - (lut[i].day_of_month - 1)));
        else
            return toDayNum(LUTIndex(i - (lut[i].day_of_month - 1)));
    }

    /// Round up to last day of month.
    template <typename DateOrTime>
    inline Time toLastDayOfMonth(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return lut_saturated[i + (lut[i].days_in_month - lut[i].day_of_month)].date;
        else
            return lut[i + (lut[i].days_in_month - lut[i].day_of_month)].date;
    }

    template <typename DateOrTime>
    inline auto toLastDayNumOfMonth(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(i + (lut[i].days_in_month - lut[i].day_of_month)));
        else
            return toDayNum(LUTIndex(i + (lut[i].days_in_month - lut[i].day_of_month)));
    }

    /// Round down to start of quarter.
    template <typename DateOrTime>
    inline auto toFirstDayNumOfQuarter(DateOrTime v) const
    {
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(toFirstDayOfQuarterIndex(v)));
        else
            return toDayNum(LUTIndex(toFirstDayOfQuarterIndex(v)));
    }

    template <typename DateOrTime>
    inline LUTIndex toFirstDayOfQuarterIndex(DateOrTime v) const
    {
        LUTIndex index = toLUTIndex(v);
        size_t month_inside_quarter = (lut[index].month - 1) % 3;

        index -= lut[index].day_of_month;
        while (month_inside_quarter)
        {
            index -= lut[index].day_of_month;
            --month_inside_quarter;
        }

        return index + 1;
    }

    template <typename DateOrTime>
    inline Time toFirstDayOfQuarter(DateOrTime v) const
    {
        return toDate(toFirstDayOfQuarterIndex(v));
    }

    /// Round down to start of year.
    inline Time toFirstDayOfYear(Time t) const
    {
        return lut[years_lut[lut[findIndex(t)].year - DATE_LUT_MIN_YEAR]].date;
    }

    template <typename DateOrTime>
    inline LUTIndex toFirstDayNumOfYearIndex(DateOrTime v) const
    {
        return years_lut[lut[toLUTIndex(v)].year - DATE_LUT_MIN_YEAR];
    }

    template <typename DateOrTime>
    inline auto toFirstDayNumOfYear(DateOrTime v) const
    {
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(toFirstDayNumOfYearIndex(v)));
        else
            return toDayNum(LUTIndex(toFirstDayNumOfYearIndex(v)));
    }

    inline Time toFirstDayOfNextMonth(Time t) const
    {
        LUTIndex index = findIndex(t);
        index += 32 - lut[index].day_of_month;
        return lut[index - (lut[index].day_of_month - 1)].date;
    }

    inline Time toFirstDayOfPrevMonth(Time t) const
    {
        LUTIndex index = findIndex(t);
        index -= lut[index].day_of_month;
        return lut[index - (lut[index].day_of_month - 1)].date;
    }

    template <typename DateOrTime>
    inline UInt8 daysInMonth(DateOrTime value) const
    {
        const LUTIndex i = toLUTIndex(value);
        return lut[i].days_in_month;
    }

    inline UInt8 daysInMonth(Int16 year, UInt8 month) const
    {
        UInt16 idx = year - DATE_LUT_MIN_YEAR;
        if (unlikely(idx >= DATE_LUT_YEARS))
            return 31;  /// Implementation specific behaviour on overflow.

        /// 32 makes arithmetic more simple.
        const auto any_day_of_month = years_lut[year - DATE_LUT_MIN_YEAR] + 32 * (month - 1);
        return lut[any_day_of_month].days_in_month;
    }

    /** Round to start of day, then shift for specified amount of days.
      */
    inline Time toDateAndShift(Time t, Int32 days) const
    {
        return lut[findIndex(t) + days].date;
    }

    inline Time toTime(Time t) const
    {
        const LUTIndex index = findIndex(t);

        Time res = t - lut[index].date;

        if (res >= lut[index].time_at_offset_change())
            res += lut[index].amount_of_offset_change();

        return res - offset_at_start_of_epoch; /// Starting at 1970-01-01 00:00:00 local time.
    }

    inline unsigned toHour(Time t) const
    {
        const LUTIndex index = findIndex(t);

        Time time = t - lut[index].date;

        if (time >= lut[index].time_at_offset_change())
            time += lut[index].amount_of_offset_change();

        unsigned res = static_cast<unsigned>(time / 3600);

        /// In case time was changed backwards at the start of next day, we will repeat the hour 23.
        return res <= 23 ? res : 23;
    }

    /** Calculating offset from UTC in seconds.
      * which means Using the same literal time of "t" to get the corresponding timestamp in UTC,
      * then subtract the former from the latter to get the offset result.
      * The boundaries when meets DST(daylight saving time) change should be handled very carefully.
      */
    inline Time timezoneOffset(Time t) const
    {
        const LUTIndex index = findIndex(t);

        /// Calculate daylight saving offset first.
        /// Because the "amount_of_offset_change" in LUT entry only exists in the change day, it's costly to scan it from the very begin.
        /// but we can figure out all the accumulated offsets from 1970-01-01 to that day just by get the whole difference between lut[].date,
        /// and then, we can directly subtract multiple 86400s to get the real DST offsets for the leap seconds is not considered now.
        Time res = (lut[index].date - lut[daynum_offset_epoch].date) % 86400;

        /// As so far to know, the maximal DST offset couldn't be more than 2 hours, so after the modulo operation the remainder
        /// will sits between [-offset --> 0 --> offset] which respectively corresponds to moving clock forward or backward.
        res = res > 43200 ? (86400 - res) : (0 - res);

        /// Check if has a offset change during this day. Add the change when cross the line
        if (lut[index].amount_of_offset_change() != 0 && t >= lut[index].date + lut[index].time_at_offset_change())
            res += lut[index].amount_of_offset_change();

        return res + offset_at_start_of_epoch;
    }


    inline unsigned toSecond(Time t) const
    {
        if (likely(offset_is_whole_number_of_minutes_during_epoch))
        {
            Time res = t % 60;
            if (likely(res >= 0))
                return static_cast<unsigned>(res);
            return static_cast<unsigned>(res) + 60;
        }

        LUTIndex index = findIndex(t);
        Time time = t - lut[index].date;

        if (time >= lut[index].time_at_offset_change())
            time += lut[index].amount_of_offset_change();

        return time % 60;
    }

    inline unsigned toMinute(Time t) const
    {
        if (t >= 0 && offset_is_whole_number_of_hours_during_epoch)
            return (t / 60) % 60;

        /// To consider the DST changing situation within this day
        /// also make the special timezones with no whole hour offset such as 'Australia/Lord_Howe' been taken into account.

        LUTIndex index = findIndex(t);
        UInt32 time = static_cast<UInt32>(t - lut[index].date);

        if (time >= lut[index].time_at_offset_change())
            time += lut[index].amount_of_offset_change();

        return time / 60 % 60;
    }

    /// NOTE: Assuming timezone offset is a multiple of 15 minutes.
    template <typename DateOrTime>
    DateOrTime toStartOfMinute(DateOrTime t) const { return toStartOfMinuteInterval(t, 1); }
    template <typename DateOrTime>
    DateOrTime toStartOfFiveMinutes(DateOrTime t) const { return toStartOfMinuteInterval(t, 5); }
    template <typename DateOrTime>
    DateOrTime toStartOfFifteenMinutes(DateOrTime t) const { return toStartOfMinuteInterval(t, 15); }
    template <typename DateOrTime>
    DateOrTime toStartOfTenMinutes(DateOrTime t) const { return toStartOfMinuteInterval(t, 10); }
    template <typename DateOrTime>
    DateOrTime toStartOfHour(DateOrTime t) const { return roundDown(t, 3600); }

    /** Number of calendar day since the beginning of UNIX epoch (1970-01-01 is zero)
      * We use just two bytes for it. It covers the range up to 2105 and slightly more.
      *
      * This is "calendar" day, it itself is independent of time zone
      * (conversion from/to unix timestamp will depend on time zone,
      *  because the same calendar day starts/ends at different timestamps in different time zones)
      */

    inline Time fromDayNum(DayNum d) const { return lut_saturated[toLUTIndex(d)].date; }
    inline Time fromDayNum(ExtendedDayNum d) const { return lut[toLUTIndex(d)].date; }

    template <typename DateOrTime>
    inline Time toDate(DateOrTime v) const
    {
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return lut_saturated[toLUTIndex(v)].date;
        else
            return lut[toLUTIndex(v)].date;
    }

    template <typename DateOrTime>
    inline UInt8 toMonth(DateOrTime v) const { return lut[toLUTIndex(v)].month; }

    template <typename DateOrTime>
    inline UInt8 toQuarter(DateOrTime v) const { return (lut[toLUTIndex(v)].month - 1) / 3 + 1; }

    template <typename DateOrTime>
    inline Int16 toYear(DateOrTime v) const { return lut[toLUTIndex(v)].year; }

    /// 1-based, starts on Monday
    template <typename DateOrTime>
    inline UInt8 toDayOfWeek(DateOrTime v) const { return lut[toLUTIndex(v)].day_of_week; }

    template <typename DateOrTime>
    inline UInt8 toDayOfWeek(DateOrTime v, UInt8 week_day_mode) const
    {
        WeekDayMode mode = check_week_day_mode(week_day_mode);

        UInt8 res = toDayOfWeek(v);
        using enum WeekDayMode;
        bool start_from_sunday = (mode == WeekStartsSunday0 || mode == WeekStartsSunday1);
        bool zero_based = (mode == WeekStartsMonday0 || mode == WeekStartsSunday0);

        if (start_from_sunday)
            res = res % 7 + 1;
        if (zero_based)
            --res;

        return res;
    }

    template <typename DateOrTime>
    inline UInt8 toDayOfMonth(DateOrTime v) const { return lut[toLUTIndex(v)].day_of_month; }

    template <typename DateOrTime>
    inline UInt16 toDayOfYear(DateOrTime v) const
    {
        // TODO: different overload for ExtendedDayNum
        const LUTIndex i = toLUTIndex(v);
        return static_cast<UInt16>(i + 1 - toFirstDayNumOfYearIndex(i));
    }

    /// Number of week from some fixed moment in the past. Week begins at monday.
    /// (round down to monday and divide DayNum by 7; we made an assumption,
    ///  that in domain of the function there was no weeks with any other number of days than 7)
    template <typename DateOrTime>
    inline Int32 toRelativeWeekNum(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        /// We add 8 to avoid underflow at beginning of unix epoch.
        return toDayNum(i + (8 - toDayOfWeek(i))) / 7;
    }

    /// Get year that contains most of the current week. Week begins at monday.
    template <typename DateOrTime>
    inline Int16 toISOYear(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        /// That's effectively the year of thursday of current week.
        return toYear(toLUTIndex(i + (4 - toDayOfWeek(i))));
    }

    /// ISO year begins with a monday of the week that is contained more than by half in the corresponding calendar year.
    /// Example: ISO year 2019 begins at 2018-12-31. And ISO year 2017 begins at 2017-01-02.
    /// https://en.wikipedia.org/wiki/ISO_week_date
    template <typename DateOrTime>
    inline LUTIndex toFirstDayNumOfISOYearIndex(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        auto iso_year = toISOYear(i);

        const auto first_day_of_year = years_lut[iso_year - DATE_LUT_MIN_YEAR];
        auto first_day_of_week_of_year = lut[first_day_of_year].day_of_week;

        return LUTIndex{first_day_of_week_of_year <= 4
            ? first_day_of_year + (1 - first_day_of_week_of_year)
            : first_day_of_year + (8 - first_day_of_week_of_year)};
    }

    template <typename DateOrTime>
    inline auto toFirstDayNumOfISOYear(DateOrTime v) const
    {
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(toFirstDayNumOfISOYearIndex(v)));
        else
            return toDayNum(LUTIndex(toFirstDayNumOfISOYearIndex(v)));
    }

    inline Time toFirstDayOfISOYear(Time t) const
    {
        return lut[toFirstDayNumOfISOYearIndex(t)].date;
    }

    /// ISO 8601 week number. Week begins at monday.
    /// The week number 1 is the first week in year that contains 4 or more days (that's more than half).
    template <typename DateOrTime>
    inline UInt8 toISOWeek(DateOrTime v) const
    {
        return 1 + (toFirstDayNumOfWeek(v) - toDayNum(toFirstDayNumOfISOYearIndex(v))) / 7;
    }

    /*
      The bits in week_mode has the following meaning:
       WeekModeFlag::MONDAY_FIRST (0)  If not set Sunday is first day of week
                      If set Monday is first day of week
       WeekModeFlag::YEAR (1) If not set Week is in range 0-53

        Week 0 is returned for the the last week of the previous year (for
        a date at start of january) In this case one can get 53 for the
        first week of next year.  This flag ensures that the week is
        relevant for the given year. Note that this flag is only
        relevant if WeekModeFlag::JANUARY is not set.

                  If set Week is in range 1-53.

        In this case one may get week 53 for a date in January (when
        the week is that last week of previous year) and week 1 for a
        date in December.

      WeekModeFlag::FIRST_WEEKDAY (2) If not set Weeks are numbered according
                        to ISO 8601:1988
                  If set The week that contains the first
                        'first-day-of-week' is week 1.

      WeekModeFlag::NEWYEAR_DAY (3) If not set no meaning
                  If set The week that contains the January 1 is week 1.
                            Week is in range 1-53.
                            And ignore WeekModeFlag::YEAR, WeekModeFlag::FIRST_WEEKDAY

        ISO 8601:1988 means that if the week containing January 1 has
        four or more days in the new year, then it is week 1;
        Otherwise it is the last week of the previous year, and the
        next week is week 1.
    */
    template <typename DateOrTime>
    inline YearWeek toYearWeek(DateOrTime v, UInt8 week_mode) const
    {
        const bool newyear_day_mode = week_mode & static_cast<UInt8>(WeekModeFlag::NEWYEAR_DAY);
        week_mode = check_week_mode(week_mode);
        const bool monday_first_mode = week_mode & static_cast<UInt8>(WeekModeFlag::MONDAY_FIRST);
        bool week_year_mode = week_mode & static_cast<UInt8>(WeekModeFlag::YEAR);
        const bool first_weekday_mode = week_mode & static_cast<UInt8>(WeekModeFlag::FIRST_WEEKDAY);

        const LUTIndex i = toLUTIndex(v);

        // Calculate week number of WeekModeFlag::NEWYEAR_DAY mode
        if (newyear_day_mode)
        {
            return toYearWeekOfNewyearMode(i, monday_first_mode);
        }

        YearWeek yw(toYear(i), 0);
        UInt16 days = 0;
        const auto day_number = makeDayNum(yw.first, toMonth(i), toDayOfMonth(i));
        auto first_day_number = makeDayNum(yw.first, 1, 1);

        // 0 for monday, 1 for tuesday ...
        // get weekday from first day in year.
        UInt8 weekday = calc_weekday(first_day_number, !monday_first_mode);

        if (toMonth(i) == 1 && toDayOfMonth(i) <= static_cast<UInt32>(7 - weekday))
        {
            if (!week_year_mode && ((first_weekday_mode && weekday != 0) || (!first_weekday_mode && weekday >= 4)))
                return yw;
            week_year_mode = true;
            --yw.first;
            days = calc_days_in_year(yw.first);
            first_day_number -= days;
            weekday = (weekday + 53 * 7 - days) % 7;
        }

        if ((first_weekday_mode && weekday != 0) || (!first_weekday_mode && weekday >= 4))
            days = day_number - (first_day_number + (7 - weekday));
        else
            days = day_number - (first_day_number - weekday);

        if (week_year_mode && days >= 52 * 7)
        {
            weekday = (weekday + calc_days_in_year(yw.first)) % 7;
            if ((!first_weekday_mode && weekday < 4) || (first_weekday_mode && weekday == 0))
            {
                ++yw.first;
                yw.second = 1;
                return yw;
            }
        }

        yw.second = days / 7 + 1;
        return yw;
    }

    /// Calculate week number of WeekModeFlag::NEWYEAR_DAY mode
    /// The week number 1 is the first week in year that contains January 1,
    template <typename DateOrTime>
    inline YearWeek toYearWeekOfNewyearMode(DateOrTime v, bool monday_first_mode) const
    {
        YearWeek yw(0, 0);
        UInt16 offset_day = monday_first_mode ? 0U : 1U;

        const LUTIndex i = LUTIndex(v);

        // Checking the week across the year
        yw.first = toYear(i + (7 - toDayOfWeek(i + offset_day)));

        auto first_day = makeLUTIndex(yw.first, 1, 1);
        auto this_day = i;

        // TODO: do not perform calculations in terms of DayNum, since that would under/overflow for extended range.
        if (monday_first_mode)
        {
            // Rounds down a date to the nearest Monday.
            first_day = toFirstDayNumOfWeek(first_day);
            this_day = toFirstDayNumOfWeek(i);
        }
        else
        {
            // Rounds down a date to the nearest Sunday.
            if (toDayOfWeek(first_day) != 7)
                first_day = ExtendedDayNum(first_day - toDayOfWeek(first_day));
            if (toDayOfWeek(i) != 7)
                this_day = ExtendedDayNum(i - toDayOfWeek(i));
        }
        yw.second = (this_day - first_day) / 7 + 1;
        return yw;
    }

    /// Get first day of week with week_mode, return Sunday or Monday
    template <typename DateOrTime>
    inline auto toFirstDayNumOfWeek(DateOrTime v, UInt8 week_mode) const
    {
        bool monday_first_mode = week_mode & static_cast<UInt8>(WeekModeFlag::MONDAY_FIRST);
        if (monday_first_mode)
        {
            return toFirstDayNumOfWeek(v);
        }
        else
        {
            const auto day_of_week = toDayOfWeek(v);
            if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
                return (day_of_week != 7) ? DayNum(saturateMinus(v, day_of_week)) : toDayNum(v);
            else
                return (day_of_week != 7) ? ExtendedDayNum(v - day_of_week) : toDayNum(v);
        }
    }

    /// Get last day of week with week_mode, return Saturday or Sunday
    template <typename DateOrTime>
    inline auto toLastDayNumOfWeek(DateOrTime v, UInt8 week_mode) const
    {
        bool monday_first_mode = week_mode & static_cast<UInt8>(WeekModeFlag::MONDAY_FIRST);
        if (monday_first_mode)
        {
            return toLastDayNumOfWeek(v);
        }
        else
        {
            const auto day_of_week = toDayOfWeek(v);
            v += 6;
            if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
                return (day_of_week != 7) ? DayNum(saturateMinus(v, day_of_week)) : toDayNum(v);
            else
                return (day_of_week != 7) ? ExtendedDayNum(v - day_of_week) : toDayNum(v);
        }
    }

    /// Check and change mode to effective.
    inline UInt8 check_week_mode(UInt8 mode) const /// NOLINT
    {
        UInt8 week_format = (mode & 7);
        if (!(week_format & static_cast<UInt8>(WeekModeFlag::MONDAY_FIRST)))
            week_format ^= static_cast<UInt8>(WeekModeFlag::FIRST_WEEKDAY);
        return week_format;
    }

    /// Check and change mode to effective.
    inline WeekDayMode check_week_day_mode(UInt8 mode) const /// NOLINT
    {
        return static_cast<WeekDayMode>(mode & 3);
    }

    /** Calculate weekday from d.
      * Returns 0 for monday, 1 for tuesday...
      */
    template <typename DateOrTime>
    inline UInt8 calc_weekday(DateOrTime v, bool sunday_first_day_of_week) const /// NOLINT
    {
        const LUTIndex i = toLUTIndex(v);
        if (!sunday_first_day_of_week)
            return toDayOfWeek(i) - 1;
        else
            return toDayOfWeek(i + 1) - 1;
    }

    /// Calculate days in one year.
    inline UInt16 calc_days_in_year(Int32 year) const /// NOLINT
    {
        return ((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year)) ? 366 : 365);
    }

    /// Number of month from some fixed moment in the past (year * 12 + month)
    template <typename DateOrTime>
    inline Int32 toRelativeMonthNum(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        return lut[i].year * 12 + lut[i].month;
    }

    template <typename DateOrTime>
    inline Int32 toRelativeQuarterNum(DateOrTime v) const
    {
        const LUTIndex i = toLUTIndex(v);
        return lut[i].year * 4 + (lut[i].month - 1) / 3;
    }

    /// We count all hour-length intervals, unrelated to offset changes.
    inline Time toRelativeHourNum(Time t) const
    {
        if (t >= 0 && offset_is_whole_number_of_hours_during_epoch)
            return t / 3600;

        /// Assume that if offset was fractional, then the fraction is the same as at the beginning of epoch.
        /// NOTE This assumption is false for "Pacific/Pitcairn" and "Pacific/Kiritimati" time zones.
        return (t + DATE_LUT_ADD + 86400 - offset_at_start_of_epoch) / 3600 - (DATE_LUT_ADD / 3600);
    }

    template <typename DateOrTime>
    inline Time toRelativeHourNum(DateOrTime v) const
    {
        return toRelativeHourNum(lut[toLUTIndex(v)].date);
    }

    /// The same formula is used for positive time (after Unix epoch) and negative time (before Unix epoch).
    /// It’s needed for correct work of dateDiff function.
    inline Time toStableRelativeHourNum(Time t) const
    {
        return (t + DATE_LUT_ADD + 86400 - offset_at_start_of_epoch) / 3600 - (DATE_LUT_ADD / 3600);
    }

    template <typename DateOrTime>
    inline Time toStableRelativeHourNum(DateOrTime v) const
    {
        return toStableRelativeHourNum(lut[toLUTIndex(v)].date);
    }

    inline Time toRelativeMinuteNum(Time t) const /// NOLINT
    {
        return (t + DATE_LUT_ADD) / 60 - (DATE_LUT_ADD / 60);
    }

    template <typename DateOrTime>
    inline Time toRelativeMinuteNum(DateOrTime v) const
    {
        return toRelativeMinuteNum(lut[toLUTIndex(v)].date);
    }

    template <typename DateOrTime>
    inline auto toStartOfYearInterval(DateOrTime v, UInt64 years) const
    {
        if (years == 1)
            return toFirstDayNumOfYear(v);

        const LUTIndex i = toLUTIndex(v);

        UInt16 year = lut[i].year / years * years;

        /// For example, rounding down 1925 to 100 years will be 1900, but it's less than min supported year.
        if (unlikely(year < DATE_LUT_MIN_YEAR))
            year = DATE_LUT_MIN_YEAR;

        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
            return toDayNum(LUTIndexWithSaturation(years_lut[year - DATE_LUT_MIN_YEAR]));
        else
            return toDayNum(years_lut[year - DATE_LUT_MIN_YEAR]);
    }

    template <typename Date>
    requires std::is_same_v<Date, DayNum> || std::is_same_v<Date, ExtendedDayNum>
    inline auto toStartOfQuarterInterval(Date d, UInt64 quarters) const
    {
        if (quarters == 1)
            return toFirstDayNumOfQuarter(d);
        return toStartOfMonthInterval(d, quarters * 3);
    }

    template <typename Date>
    requires std::is_same_v<Date, DayNum> || std::is_same_v<Date, ExtendedDayNum>
    inline auto toStartOfMonthInterval(Date d, UInt64 months) const
    {
        if (months == 1)
            return toFirstDayNumOfMonth(d);
        const Values & values = lut[toLUTIndex(d)];
        UInt32 month_total_index = (values.year - DATE_LUT_MIN_YEAR) * 12 + values.month - 1;
        if constexpr (std::is_same_v<Date, DayNum>)
            return toDayNum(LUTIndexWithSaturation(years_months_lut[month_total_index / months * months]));
        else
            return toDayNum(years_months_lut[month_total_index / months * months]);
    }

    template <typename Date>
    requires std::is_same_v<Date, DayNum> || std::is_same_v<Date, ExtendedDayNum>
    inline auto toStartOfWeekInterval(Date d, UInt64 weeks) const
    {
        if (weeks == 1)
            return toFirstDayNumOfWeek(d);
        UInt64 days = weeks * 7;
        // January 1st 1970 was Thursday so we need this 4-days offset to make weeks start on Monday.
        if constexpr (std::is_same_v<Date, DayNum>)
            return DayNum(4 + (d - 4) / days * days);
        else
            return ExtendedDayNum(static_cast<Int32>(4 + (d - 4) / days * days));
    }

    template <typename Date>
    requires std::is_same_v<Date, DayNum> || std::is_same_v<Date, ExtendedDayNum>
    inline Time toStartOfDayInterval(Date d, UInt64 days) const
    {
        if (days == 1)
            return toDate(d);
        if constexpr (std::is_same_v<Date, DayNum>)
            return lut_saturated[toLUTIndex(ExtendedDayNum(static_cast<Int32>(d / days * days)))].date;
        else
            return lut[toLUTIndex(ExtendedDayNum(static_cast<Int32>(d / days * days)))].date;
    }

    template <typename DateOrTime>
    DateOrTime toStartOfHourInterval(DateOrTime t, UInt64 hours) const
    {
        if (hours == 1)
            return toStartOfHour(t);

        /** We will round the hour number since the midnight.
          * It may split the day into non-equal intervals.
          * For example, if we will round to 11-hour interval,
          * the day will be split to the intervals 00:00:00..10:59:59, 11:00:00..21:59:59, 22:00:00..23:59:59.
          * In case of daylight saving time or other transitions,
          * the intervals can be shortened or prolonged to the amount of transition.
          */

        UInt64 seconds = hours * 3600;

        const LUTIndex index = findIndex(t);
        const Values & values = lut[index];

        Time time = t - values.date;
        if (time >= values.time_at_offset_change())
        {
            /// Align to new hour numbers before rounding.
            time += values.amount_of_offset_change();
            time = time / seconds * seconds;

            /// Should subtract the shift back but only if rounded time is not before shift.
            if (time >= values.time_at_offset_change())
            {
                time -= values.amount_of_offset_change();

                /// With cutoff at the time of the shift. Otherwise we may end up with something like 23:00 previous day.
                if (time < values.time_at_offset_change())
                    time = values.time_at_offset_change();
            }
        }
        else
        {
            time = time / seconds * seconds;
        }

        Time res = values.date + time;
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
        {
            if (unlikely(res < 0))
                return 0;
            return static_cast<DateOrTime>(res);
        }
        else
            return res;
    }

    template <typename DateOrTime>
    DateOrTime toStartOfMinuteInterval(DateOrTime t, UInt64 minutes) const
    {
        Int64 divisor = 60 * minutes;
        if (likely(offset_is_whole_number_of_minutes_during_epoch))
        {
            if (likely(t >= 0))
                return static_cast<DateOrTime>(t / divisor * divisor);
            return static_cast<DateOrTime>((t + 1 - divisor) / divisor * divisor);
        }

        Time date = find(t).date;
        Time res = date + (t - date) / divisor * divisor;
        if constexpr (std::is_unsigned_v<DateOrTime> || std::is_same_v<DateOrTime, DayNum>)
        {
            if (unlikely(res < 0))
                return 0;
            return static_cast<UInt32>(res);
        }
        else
            return res;
    }

    template <typename DateOrTime>
    DateOrTime toStartOfSecondInterval(DateOrTime t, UInt64 seconds) const
    {
        if (seconds == 1)
            return t;
        if (seconds % 60 == 0)
            return toStartOfMinuteInterval(t, seconds / 60);

        return static_cast<DateOrTime>(roundDown(t, seconds));
    }

    inline LUTIndex makeLUTIndex(Int16 year, UInt8 month, UInt8 day_of_month) const
    {
        if (unlikely(year < DATE_LUT_MIN_YEAR || month < 1 || month > 12 || day_of_month < 1 || day_of_month > 31))
            return LUTIndex(0);

        if (unlikely(year > DATE_LUT_MAX_YEAR))
            return LUTIndex(DATE_LUT_SIZE - 1);

        auto year_lut_index = (year - DATE_LUT_MIN_YEAR) * 12 + month - 1;
        UInt32 index = years_months_lut[year_lut_index].toUnderType() + day_of_month - 1;
        /// When date is out of range, default value is DATE_LUT_SIZE - 1 (2299-12-31)
        return LUTIndex{std::min(index, static_cast<UInt32>(DATE_LUT_SIZE - 1))};
    }

    /// Create DayNum from year, month, day of month.
    inline ExtendedDayNum makeDayNum(Int16 year, UInt8 month, UInt8 day_of_month, Int32 default_error_day_num = 0) const
    {
        if (unlikely(year < DATE_LUT_MIN_YEAR || month < 1 || month > 12 || day_of_month < 1 || day_of_month > 31))
            return ExtendedDayNum(default_error_day_num);

        return toDayNum(makeLUTIndex(year, month, day_of_month));
    }

    inline Time makeDate(Int16 year, UInt8 month, UInt8 day_of_month) const
    {
        return lut[makeLUTIndex(year, month, day_of_month)].date;
    }

    /** Does not accept daylight saving time as argument: in case of ambiguity, it choose greater timestamp.
      */
    inline Time makeDateTime(Int16 year, UInt8 month, UInt8 day_of_month, UInt8 hour, UInt8 minute, UInt8 second) const
    {
        size_t index = makeLUTIndex(year, month, day_of_month);
        Time time_offset = hour * 3600 + minute * 60 + second;

        if (time_offset >= lut[index].time_at_offset_change())
            time_offset -= lut[index].amount_of_offset_change();

        return lut[index].date + time_offset;
    }

    template <typename DateOrTime>
    inline const Values & getValues(DateOrTime v) const { return lut[toLUTIndex(v)]; }

    template <typename DateOrTime>
    inline UInt32 toNumYYYYMM(DateOrTime v) const
    {
        const Values & values = getValues(v);
        return values.year * 100 + values.month;
    }

    template <typename DateOrTime>
    inline UInt32 toNumYYYYMMDD(DateOrTime v) const
    {
        const Values & values = getValues(v);
        return values.year * 10000 + values.month * 100 + values.day_of_month;
    }

    inline Time YYYYMMDDToDate(UInt32 num) const /// NOLINT
    {
        return makeDate(num / 10000, num / 100 % 100, num % 100);
    }

    inline ExtendedDayNum YYYYMMDDToDayNum(UInt32 num) const /// NOLINT
    {
        return makeDayNum(num / 10000, num / 100 % 100, num % 100);
    }


    struct DateComponents
    {
        uint16_t year;
        uint8_t month;
        uint8_t day;
    };

    struct TimeComponents
    {
        uint8_t hour;
        uint8_t minute;
        uint8_t second;
    };

    struct DateTimeComponents
    {
        DateComponents date;
        TimeComponents time;
    };

    inline DateComponents toDateComponents(Time t) const
    {
        const Values & values = getValues(t);
        return { values.year, values.month, values.day_of_month };
    }

    inline DateTimeComponents toDateTimeComponents(Time t) const
    {
        const LUTIndex index = findIndex(t);
        const Values & values = lut[index];

        DateTimeComponents res;

        res.date.year = values.year;
        res.date.month = values.month;
        res.date.day = values.day_of_month;

        Time time = t - values.date;
        if (time >= values.time_at_offset_change())
            time += values.amount_of_offset_change();

        if (unlikely(time < 0))
        {
            res.time.second = 0;
            res.time.minute = 0;
            res.time.hour = 0;
        }
        else
        {
            res.time.second = time % 60;
            res.time.minute = time / 60 % 60;
            res.time.hour = time / 3600;
        }

        /// In case time was changed backwards at the start of next day, we will repeat the hour 23.
        if (unlikely(res.time.hour > 23))
            res.time.hour = 23;

        return res;
    }

    template <typename DateOrTime>
    inline DateTimeComponents toDateTimeComponents(DateOrTime v) const
    {
        return toDateTimeComponents(lut[toLUTIndex(v)].date);
    }

    inline UInt64 toNumYYYYMMDDhhmmss(Time t) const
    {
        DateTimeComponents components = toDateTimeComponents(t);

        return
              components.time.second
            + components.time.minute * 100
            + components.time.hour * 10000
            + UInt64(components.date.day) * 1000000
            + UInt64(components.date.month) * 100000000
            + UInt64(components.date.year) * 10000000000;
    }

    inline Time YYYYMMDDhhmmssToTime(UInt64 num) const /// NOLINT
    {
        return makeDateTime(
            num / 10000000000,
            num / 100000000 % 100,
            num / 1000000 % 100,
            num / 10000 % 100,
            num / 100 % 100,
            num % 100);
    }

    /// Adding calendar intervals.
    /// Implementation specific behaviour when delta is too big.

    inline NO_SANITIZE_UNDEFINED Time addDays(Time t, Int64 delta) const
    {
        const LUTIndex index = findIndex(t);
        const Values & values = lut[index];

        Time time = t - values.date;
        if (time >= values.time_at_offset_change())
            time += values.amount_of_offset_change();

        const LUTIndex new_index = index + delta;

        if (time >= lut[new_index].time_at_offset_change())
            time -= lut[new_index].amount_of_offset_change();

        return lut[new_index].date + time;
    }

    inline NO_SANITIZE_UNDEFINED Time addWeeks(Time t, Int64 delta) const
    {
        return addDays(t, delta * 7);
    }

    inline UInt8 saturateDayOfMonth(Int16 year, UInt8 month, UInt8 day_of_month) const
    {
        if (likely(day_of_month <= 28))
            return day_of_month;

        UInt8 days_in_month = daysInMonth(year, month);

        if (day_of_month > days_in_month)
            day_of_month = days_in_month;

        return day_of_month;
    }

    template <typename DateOrTime>
    inline LUTIndex NO_SANITIZE_UNDEFINED addMonthsIndex(DateOrTime v, Int64 delta) const
    {
        const Values & values = lut[toLUTIndex(v)];

        Int64 month = values.month + delta;

        if (month > 0)
        {
            auto year = values.year + (month - 1) / 12;
            month = ((month - 1) % 12) + 1;
            auto day_of_month = saturateDayOfMonth(year, month, values.day_of_month);

            return makeLUTIndex(year, month, day_of_month);
        }
        else
        {
            auto year = values.year - (12 - month) / 12;
            month = 12 - (-month % 12);
            auto day_of_month = saturateDayOfMonth(year, month, values.day_of_month);

            return makeLUTIndex(year, month, day_of_month);
        }
    }

    /// If resulting month has less deys than source month, then saturation can happen.
    /// Example: 31 Aug + 1 month = 30 Sep.
    template <typename DateTime>
    requires std::is_same_v<DateTime, UInt32> || std::is_same_v<DateTime, Int64> || std::is_same_v<DateTime, time_t>
    inline Time NO_SANITIZE_UNDEFINED addMonths(DateTime t, Int64 delta) const
    {
        const auto result_day = addMonthsIndex(t, delta);

        const LUTIndex index = findIndex(t);
        const Values & values = lut[index];

        Time time = t - values.date;
        if (time >= values.time_at_offset_change())
            time += values.amount_of_offset_change();

        if (time >= lut[result_day].time_at_offset_change())
            time -= lut[result_day].amount_of_offset_change();

        auto res = lut[result_day].date + time;
        if constexpr (std::is_same_v<DateTime, UInt32>)
        {
            /// Common compiler should generate branchless code for this saturation operation.
            return res <= 0 ? 0 : res;
        }
        else
            return res;
    }

    template <typename Date>
    requires std::is_same_v<Date, DayNum> || std::is_same_v<Date, ExtendedDayNum>
    inline auto NO_SANITIZE_UNDEFINED addMonths(Date d, Int64 delta) const
    {
        if constexpr (std::is_same_v<Date, DayNum>)
            return toDayNum(LUTIndexWithSaturation(addMonthsIndex(d, delta)));
        else
            return toDayNum(addMonthsIndex(d, delta));
    }

    template <typename DateOrTime>
    inline auto NO_SANITIZE_UNDEFINED addQuarters(DateOrTime d, Int64 delta) const
    {
        return addMonths(d, delta * 3);
    }

    template <typename DateOrTime>
    inline LUTIndex NO_SANITIZE_UNDEFINED addYearsIndex(DateOrTime v, Int64 delta) const
    {
        const Values & values = lut[toLUTIndex(v)];

        auto year = values.year + delta;
        auto month = values.month;
        auto day_of_month = values.day_of_month;

        /// Saturation to 28 Feb can happen.
        if (unlikely(day_of_month == 29 && month == 2))
            day_of_month = saturateDayOfMonth(year, month, day_of_month);

        return makeLUTIndex(year, month, day_of_month);
    }

    /// Saturation can occur if 29 Feb is mapped to non-leap year.
    template <typename DateTime>
    requires std::is_same_v<DateTime, UInt32> || std::is_same_v<DateTime, Int64> || std::is_same_v<DateTime, time_t>
    inline Time addYears(DateTime t, Int64 delta) const
    {
        auto result_day = addYearsIndex(t, delta);

        const LUTIndex index = findIndex(t);
        const Values & values = lut[index];

        Time time = t - values.date;
        if (time >= values.time_at_offset_change())
            time += values.amount_of_offset_change();

        if (time >= lut[result_day].time_at_offset_change())
            time -= lut[result_day].amount_of_offset_change();

        auto res = lut[result_day].date + time;
        if constexpr (std::is_same_v<DateTime, UInt32>)
        {
            /// Common compiler should generate branchless code for this saturation operation.
            return res <= 0 ? 0 : res;
        }
        else
            return res;
    }

    template <typename Date>
    requires std::is_same_v<Date, DayNum> || std::is_same_v<Date, ExtendedDayNum>
    inline auto addYears(Date d, Int64 delta) const
    {
        if constexpr (std::is_same_v<Date, DayNum>)
            return toDayNum(LUTIndexWithSaturation(addYearsIndex(d, delta)));
        else
            return toDayNum(addYearsIndex(d, delta));
    }


    inline std::string timeToString(Time t) const
    {
        DateTimeComponents components = toDateTimeComponents(t);

        std::string s {"0000-00-00 00:00:00"};

        s[0] += components.date.year / 1000;
        s[1] += (components.date.year / 100) % 10;
        s[2] += (components.date.year / 10) % 10;
        s[3] += components.date.year % 10;
        s[5] += components.date.month / 10;
        s[6] += components.date.month % 10;
        s[8] += components.date.day / 10;
        s[9] += components.date.day % 10;

        s[11] += components.time.hour / 10;
        s[12] += components.time.hour % 10;
        s[14] += components.time.minute / 10;
        s[15] += components.time.minute % 10;
        s[17] += components.time.second / 10;
        s[18] += components.time.second % 10;

        return s;
    }

    inline std::string dateToString(Time t) const
    {
        const Values & values = getValues(t);

        std::string s {"0000-00-00"};

        s[0] += values.year / 1000;
        s[1] += (values.year / 100) % 10;
        s[2] += (values.year / 10) % 10;
        s[3] += values.year % 10;
        s[5] += values.month / 10;
        s[6] += values.month % 10;
        s[8] += values.day_of_month / 10;
        s[9] += values.day_of_month % 10;

        return s;
    }

    inline std::string dateToString(ExtendedDayNum d) const
    {
        const Values & values = getValues(d);

        std::string s {"0000-00-00"};

        s[0] += values.year / 1000;
        s[1] += (values.year / 100) % 10;
        s[2] += (values.year / 10) % 10;
        s[3] += values.year % 10;
        s[5] += values.month / 10;
        s[6] += values.month % 10;
        s[8] += values.day_of_month / 10;
        s[9] += values.day_of_month % 10;

        return s;
    }
};