File Coverage

datetime.c
Criterion Covered Total %
statement 81 82 98.7
branch 110 156 70.5
condition n/a
subroutine n/a
pod n/a
total 191 238 80.2


line stmt bran cond sub pod time code
1             #define PERL_NO_GET_CONTEXT
2             #include "EXTERN.h"
3             #include "perl.h"
4             #include "XSUB.h"
5              
6             #include
7             #include
8              
9             #include "datetime.h"
10             #include "decimal.h"
11             #include "scalar_kind.h"
12              
13             static const int days_in_month[] = {31,28,31,30,31,30,31,31,30,31,30,31};
14              
15 1990           static int is_leap_year(int year) {
16 1990 100         return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    100          
    100          
17             }
18              
19 48           int32_t parse_date_string(pTHX_ const char *s, STRLEN len) {
20 48 100         if (!looks_like_date(s, len))
21 2 50         croak("Invalid date string (expected YYYY-MM-DD): %.*s",
22             (int)(len > 30 ? 30 : len), s);
23              
24 46           int year = (s[0]-'0')*1000 + (s[1]-'0')*100 + (s[2]-'0')*10 + (s[3]-'0');
25 46           int month = (s[5]-'0')*10 + (s[6]-'0');
26 46           int day = (s[8]-'0')*10 + (s[9]-'0');
27              
28             /* looks_like_date only validates the digit shape; reject out-of-range
29             * components so the days_in_month loop can't read past the array, and
30             * reject impossible day-of-month combinations (e.g. 2024-04-31). */
31 46 100         if (month < 1 || month > 12 || day < 1 || day > 31)
    100          
    50          
    100          
32 5 50         croak("Invalid date '%.*s': month/day out of range",
33             (int)(len > 30 ? 30 : len), s);
34 82           int max_day = days_in_month[month-1]
35 41 100         + (month == 2 && is_leap_year(year) ? 1 : 0);
    100          
36 41 100         if (day > max_day)
37 3 50         croak("Invalid date '%.*s': day %d exceeds month %d's length (%d)",
38             (int)(len > 30 ? 30 : len), s, day, month, max_day);
39              
40 38           int32_t days = 0;
41             int y, m;
42              
43 1860 100         for (y = 1970; y < year; y++) days += is_leap_year(y) ? 366 : 365;
    100          
44 181 100         for (y = year; y < 1970; y++) days -= is_leap_year(y) ? 366 : 365;
    100          
45              
46 183 100         for (m = 1; m < month; m++) {
47 145           days += days_in_month[m-1];
48 145 100         if (m == 2 && is_leap_year(year)) days++;
    100          
49             }
50              
51 38           days += day - 1;
52 38           return days;
53             }
54              
55 21           static void parse_time_part(pTHX_ const char *s, STRLEN len,
56             int *h, int *mi, int *se) {
57 21 50         if (len < 19) { *h = *mi = *se = 0; return; }
58             #define IS_DIGIT(c) ((c) >= '0' && (c) <= '9')
59 21 100         if ((s[10] != ' ' && s[10] != 'T') || s[13] != ':' || s[16] != ':'
    50          
    50          
    50          
60 21 50         || !IS_DIGIT(s[11]) || !IS_DIGIT(s[12])
    100          
    50          
    100          
61 19 50         || !IS_DIGIT(s[14]) || !IS_DIGIT(s[15])
    50          
    50          
    50          
62 19 50         || !IS_DIGIT(s[17]) || !IS_DIGIT(s[18]))
    50          
    50          
    100          
63 3 50         croak("Invalid datetime string: %.*s", (int)(len > 30 ? 30 : len), s);
64             #undef IS_DIGIT
65 18           *h = (s[11]-'0')*10 + (s[12]-'0');
66 18           *mi = (s[14]-'0')*10 + (s[15]-'0');
67 18           *se = (s[17]-'0')*10 + (s[18]-'0');
68             }
69              
70 5           static int parse_tz_offset(pTHX_ const char *s, STRLEN len, STRLEN pos) {
71 5 50         if (pos >= len) return 0;
72 5           char c = s[pos];
73 5 100         if (c == 'Z' || c == 'z') return 0;
    50          
74 4 100         if (c != '+' && c != '-') return 0;
    50          
75 4 100         int sign = (c == '-') ? -1 : 1;
76 4           pos++;
77 4 50         if (pos + 2 > len
78 4 50         || s[pos] < '0' || s[pos] > '9'
    50          
79 4 50         || s[pos+1] < '0' || s[pos+1] > '9')
    50          
80 0 0         croak("Invalid datetime timezone: %.*s",
81             (int)(len > 30 ? 30 : len), s);
82 4           int hh = (s[pos]-'0')*10 + (s[pos+1]-'0');
83 4           pos += 2;
84 4           int mm = 0;
85 4 50         if (pos < len && s[pos] == ':') pos++;
    100          
86 4 50         if (pos + 2 <= len
87 4 50         && s[pos] >= '0' && s[pos] <= '9'
    50          
88 4 50         && s[pos+1] >= '0' && s[pos+1] <= '9') {
    50          
89 4           mm = (s[pos]-'0')*10 + (s[pos+1]-'0');
90             }
91 4           return sign * (hh * 3600 + mm * 60);
92             }
93              
94 14           uint32_t parse_datetime_string(pTHX_ const char *s, STRLEN len) {
95 14           int32_t days = parse_date_string(aTHX_ s, len);
96             int h, mi, se;
97 13           parse_time_part(aTHX_ s, len, &h, &mi, &se);
98 11           int64_t epoch = (int64_t)days * 86400 + h * 3600 + mi * 60 + se;
99             /* Optional ISO 8601 zone marker at position 19 (after HH:MM:SS). */
100 11 100         if (len > 19) epoch -= parse_tz_offset(aTHX_ s, len, 19);
101             /* DateTime is UInt32 on the wire (1970-01-01 .. 2106-02-07). Reject
102             * out-of-range epochs explicitly rather than silently wrapping. */
103 11 100         if (epoch < 0 || epoch > 0xFFFFFFFFLL)
    100          
104 2 50         croak("DateTime out of UInt32 range "
105             "(1970-01-01 .. 2106-02-07 06:28:15): %.*s",
106             (int)(len > 30 ? 30 : len), s);
107 9           return (uint32_t)epoch;
108             }
109              
110             /* Multiply a Perl double by 10^precision, croak on Inf/NaN/overflow,
111             * return the rounded int64. Shared by the SvNOK and string-number
112             * branches of the T_DATETIME64 encode path. */
113 7           int64_t dt64_double_to_int64(pTHX_ double v, int precision) {
114 7           double d = v * decimal_pow10(precision);
115             /* Same bound as the T_DECIMAL64 path: 2^63 in double form. Strict
116             * inequality so 2^63 itself (which would overflow int64_t after
117             * llround) is rejected. */
118 7 100         if (!(isfinite(d) && d > -9.223372036854776e18 && d < 9.223372036854776e18))
    50          
    50          
119 2           croak("DateTime64 overflow (or non-finite value)");
120 5           return (int64_t)llround(d);
121             }
122              
123 8           int64_t parse_datetime64_string(pTHX_ const char *s, STRLEN len, int precision) {
124 8           int32_t days = parse_date_string(aTHX_ s, len);
125             int h, mi, se;
126 8           parse_time_part(aTHX_ s, len, &h, &mi, &se);
127              
128 7           int64_t base = (int64_t)days * 86400 + h * 3600 + mi * 60 + se;
129 7           int64_t frac = 0;
130 7           STRLEN tz_pos = 19;
131              
132 7 50         if (len > 20 && s[19] == '.') {
    50          
133             int i;
134 37 100         for (i = 0; i < precision && (20 + i) < (int)len; i++) {
    50          
135 30           char c = s[20 + i];
136 30 50         if (c < '0' || c > '9') break;
    50          
137 30           frac = frac * 10 + (c - '0');
138             }
139 7 50         for (; i < precision; i++) frac *= 10;
140             /* Skip any extra fractional digits beyond the column's precision. */
141 7           tz_pos = 20;
142 37 100         while (tz_pos < len && s[tz_pos] >= '0' && s[tz_pos] <= '9') tz_pos++;
    100          
    50          
143             }
144              
145 7 100         if (tz_pos < len) base -= parse_tz_offset(aTHX_ s, len, tz_pos);
146              
147 7 50         uint64_t scale = (precision >= 0 && precision <= 19) ? pow10_u64[precision] : 1;
    50          
148 7           return base * (int64_t)scale + frac;
149             }