qr_reader-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-mruby.txt +22 -0
  3. data/LICENSE-spinel.txt +19 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +61 -0
  6. data/ext/qr_reader_native/SPINEL_VERSION +1 -0
  7. data/ext/qr_reader_native/extconf.rb +8 -0
  8. data/ext/qr_reader_native/mruby/error.h +13 -0
  9. data/ext/qr_reader_native/mruby/internal.h +145 -0
  10. data/ext/qr_reader_native/mruby/string.h +22 -0
  11. data/ext/qr_reader_native/mruby.h +93 -0
  12. data/ext/qr_reader_native/mruby_shim.h +118 -0
  13. data/ext/qr_reader_native/qr_reader_native.c +13513 -0
  14. data/ext/qr_reader_native/qr_reader_native.h +29 -0
  15. data/ext/qr_reader_native/qr_reader_native_ext.c +598 -0
  16. data/ext/qr_reader_native/re_cased.h +71 -0
  17. data/ext/qr_reader_native/re_compile.c +5784 -0
  18. data/ext/qr_reader_native/re_ctype.h +3540 -0
  19. data/ext/qr_reader_native/re_exec.c +2190 -0
  20. data/ext/qr_reader_native/re_internal.h +671 -0
  21. data/ext/qr_reader_native/re_prop.h +4629 -0
  22. data/ext/qr_reader_native/re_spinel.c +363 -0
  23. data/ext/qr_reader_native/re_spinel.h +22 -0
  24. data/ext/qr_reader_native/re_spinel_utf8.inc +97 -0
  25. data/ext/qr_reader_native/re_utf8.c +156 -0
  26. data/ext/qr_reader_native/sp_alloc.c +1447 -0
  27. data/ext/qr_reader_native/sp_alloc.h +1196 -0
  28. data/ext/qr_reader_native/sp_argf.h +33 -0
  29. data/ext/qr_reader_native/sp_array.c +1052 -0
  30. data/ext/qr_reader_native/sp_array.h +456 -0
  31. data/ext/qr_reader_native/sp_bigint.c +5826 -0
  32. data/ext/qr_reader_native/sp_bigint.h +117 -0
  33. data/ext/qr_reader_native/sp_cold.c +5259 -0
  34. data/ext/qr_reader_native/sp_compat.h +402 -0
  35. data/ext/qr_reader_native/sp_core.c +818 -0
  36. data/ext/qr_reader_native/sp_core.h +56 -0
  37. data/ext/qr_reader_native/sp_crypto.c +877 -0
  38. data/ext/qr_reader_native/sp_crypto.h +169 -0
  39. data/ext/qr_reader_native/sp_dtoa.c +1533 -0
  40. data/ext/qr_reader_native/sp_dtoa.h +29 -0
  41. data/ext/qr_reader_native/sp_encoding_names.h +172 -0
  42. data/ext/qr_reader_native/sp_enum.h +86 -0
  43. data/ext/qr_reader_native/sp_exc.c +1185 -0
  44. data/ext/qr_reader_native/sp_exc.h +186 -0
  45. data/ext/qr_reader_native/sp_exc_ctx.h +56 -0
  46. data/ext/qr_reader_native/sp_fiber.c +889 -0
  47. data/ext/qr_reader_native/sp_fiber.h +162 -0
  48. data/ext/qr_reader_native/sp_fiber_ctx.h +84 -0
  49. data/ext/qr_reader_native/sp_format.c +695 -0
  50. data/ext/qr_reader_native/sp_format.h +83 -0
  51. data/ext/qr_reader_native/sp_gc.c +1793 -0
  52. data/ext/qr_reader_native/sp_gc.h +762 -0
  53. data/ext/qr_reader_native/sp_hash.c +131 -0
  54. data/ext/qr_reader_native/sp_hash.h +131 -0
  55. data/ext/qr_reader_native/sp_inspect.c +215 -0
  56. data/ext/qr_reader_native/sp_inspect.h +128 -0
  57. data/ext/qr_reader_native/sp_io.c +1676 -0
  58. data/ext/qr_reader_native/sp_io.h +272 -0
  59. data/ext/qr_reader_native/sp_iobuffer.c +1014 -0
  60. data/ext/qr_reader_native/sp_iobuffer.h +167 -0
  61. data/ext/qr_reader_native/sp_marshal.c +487 -0
  62. data/ext/qr_reader_native/sp_marshal.h +60 -0
  63. data/ext/qr_reader_native/sp_net.c +1029 -0
  64. data/ext/qr_reader_native/sp_net.h +205 -0
  65. data/ext/qr_reader_native/sp_pack.c +1254 -0
  66. data/ext/qr_reader_native/sp_poly_cold.c +1220 -0
  67. data/ext/qr_reader_native/sp_proc.c +410 -0
  68. data/ext/qr_reader_native/sp_proc.h +327 -0
  69. data/ext/qr_reader_native/sp_process.c +547 -0
  70. data/ext/qr_reader_native/sp_process_status.c +211 -0
  71. data/ext/qr_reader_native/sp_process_status.h +66 -0
  72. data/ext/qr_reader_native/sp_random.c +287 -0
  73. data/ext/qr_reader_native/sp_random.h +52 -0
  74. data/ext/qr_reader_native/sp_range.h +144 -0
  75. data/ext/qr_reader_native/sp_re.c +1379 -0
  76. data/ext/qr_reader_native/sp_re.h +186 -0
  77. data/ext/qr_reader_native/sp_re_names.h +59 -0
  78. data/ext/qr_reader_native/sp_sched.c +4044 -0
  79. data/ext/qr_reader_native/sp_sched.h +295 -0
  80. data/ext/qr_reader_native/sp_slab.c +1507 -0
  81. data/ext/qr_reader_native/sp_str.c +1462 -0
  82. data/ext/qr_reader_native/sp_str.h +306 -0
  83. data/ext/qr_reader_native/sp_str_crypt.c +23 -0
  84. data/ext/qr_reader_native/sp_string.c +92 -0
  85. data/ext/qr_reader_native/sp_string.h +253 -0
  86. data/ext/qr_reader_native/sp_system.c +78 -0
  87. data/ext/qr_reader_native/sp_system.h +18 -0
  88. data/ext/qr_reader_native/sp_time.c +860 -0
  89. data/ext/qr_reader_native/sp_time.h +100 -0
  90. data/ext/qr_reader_native/sp_types.h +292 -0
  91. data/ext/qr_reader_native/spinel_rt.h +19017 -0
  92. data/ext/qr_reader_native/unicase.c +399 -0
  93. data/ext/qr_reader_native/unicase.h +901 -0
  94. data/lib/qr_reader/native.rb +6 -0
  95. metadata +153 -0
@@ -0,0 +1,860 @@
1
+ /* sp_time.c -- libc-backed Time implementations.
2
+ *
3
+ * Sibling to sp_bigint.c / sp_crypto.c. The libc value ops (construct,
4
+ * accessors, shifts) carry no runtime dependency; the formatters
5
+ * (strftime / iso8601 / zone / inspect) return GC-heap strings directly
6
+ * via sp_alloc.h, so the generated TU no longer needs buffer-copying
7
+ * trampolines for them.
8
+ */
9
+
10
+ #include <ctype.h>
11
+ #include "sp_core.h"
12
+ #include <math.h>
13
+ #include <stdio.h>
14
+ #include <stdlib.h>
15
+ #include <string.h>
16
+ #include <time.h>
17
+
18
+ #include "sp_time.h"
19
+ #include "sp_alloc.h" /* sp_str_dup_external / sp_str_empty for the GC formatters */
20
+
21
+ /* Time#floor / #ceil / #round, shared by the typed and the boxed receiver so
22
+ the two cannot drift. The no-argument form is ndigits 0: scale is 10^9 and
23
+ every mode lands the value on a whole second. */
24
+ sp_Time sp_time_round_to(sp_Time t, int64_t ndigits, int mode) {
25
+ /* CRuby names no number here, and a supported method's message is part of
26
+ what it means to be a subset of it. */
27
+ if (ndigits < 0) sp_raise_cls("ArgumentError", "negative ndigits given");
28
+ if (ndigits >= 9) return t; /* full nanosecond resolution */
29
+ int64_t scale = 1;
30
+ for (int64_t k = ndigits; k < 9; k++) scale *= 10;
31
+ int64_t ns = t.tv_nsec;
32
+ if (mode == 0) ns = ns / scale * scale;
33
+ else if (mode == 1) { if (ns % scale) ns = (ns / scale + 1) * scale; }
34
+ else ns = (ns + scale / 2) / scale * scale;
35
+ if (ns >= 1000000000) { t.tv_sec += 1; ns -= 1000000000; }
36
+ t.tv_nsec = (int32_t)ns;
37
+ return t;
38
+ }
39
+
40
+ sp_Time sp_time_now(void) {
41
+ struct timespec ts;
42
+ clock_gettime(CLOCK_REALTIME, &ts);
43
+ return (sp_Time){ ts.tv_sec, (int32_t)ts.tv_nsec, 0 };
44
+ }
45
+
46
+ /* A sub-second argument of a whole unit or more belongs in the seconds
47
+ field, the way CRuby normalises it (#3704). */
48
+ sp_Time sp_time_norm(sp_Time t) {
49
+ int64_t ns = (int64_t)t.tv_nsec;
50
+ if (ns >= 1000000000LL || ns < 0) {
51
+ int64_t carry = ns / 1000000000LL;
52
+ ns -= carry * 1000000000LL;
53
+ if (ns < 0) { ns += 1000000000LL; carry--; }
54
+ t.tv_sec += carry;
55
+ t.tv_nsec = (int32_t)ns;
56
+ }
57
+ return t;
58
+ }
59
+ /* Add a sub-second offset that may exceed a whole second (#3704). */
60
+ sp_Time sp_time_add_nsec(sp_Time t, int64_t ns) {
61
+ t.tv_sec += ns / 1000000000LL;
62
+ t.tv_nsec = (int32_t)((int64_t)t.tv_nsec + ns % 1000000000LL);
63
+ return sp_time_norm(t);
64
+ }
65
+ sp_Time sp_time_at_int(int64_t sec) {
66
+ return (sp_Time){ sec, 0, 0 };
67
+ }
68
+
69
+ /* Time.at(Rational): the exact num/den epoch, floored to the nanosecond
70
+ (Time.at(-1r/3) is second -1, nanosecond 666666666). den is positive by
71
+ sp_Rational's normalized-sign invariant. */
72
+ sp_Time sp_time_at_div(int64_t num, int64_t den) {
73
+ if (den == 0) sp_raise_cls("ZeroDivisionError", "divided by 0");
74
+ int64_t sec = num / den;
75
+ int64_t rem = num % den;
76
+ if (rem < 0) { sec -= 1; rem += den; }
77
+ /* rem < den, so rem * 1e9 needs more than 64 bits only when den does:
78
+ a 128-bit product where the compiler has one, long double otherwise
79
+ (a 32-bit build; the quotient is below 1e9 either way) */
80
+ #if SP_HAVE_INT128
81
+ int64_t ns = (int64_t)(((sp_int128)rem * 1000000000) / den);
82
+ #else
83
+ int64_t ns = (int64_t)(((long double)rem * 1000000000.0L) / (long double)den);
84
+ #endif
85
+ return (sp_Time){ sec, (int32_t)ns, 0 };
86
+ }
87
+
88
+ /* Exact Float -> (sec, nsec) shift. CRuby converts the Float through to_r,
89
+ so the EXACT binary value of the double decides the nanosecond, floored
90
+ (Time.at(100) - 1.3 has usec 699999, not 700000, because the double
91
+ nearest -1.3 is -1.3000000000000000444...). Reproduce that with integer
92
+ math: decompose the double into mantissa * 2^exp via frexp, widen the
93
+ mantissa-nanoseconds product to 128 bits, and arithmetic-shift (which
94
+ floors negatives) instead of multiplying in double precision. */
95
+ static void sp_time_shift_ns(double secs, int64_t base_sec, int32_t base_ns,
96
+ int64_t *out_sec, int32_t *out_ns) {
97
+ /* CRuby routes the Float through to_r, so a non-finite value raises
98
+ FloatDomainError (Time.at(Float::INFINITY), t + Float::NAN, ...) rather
99
+ than reaching frexp -- where (int64_t)(NaN/Inf * 2^53) would be UB. */
100
+ if (!isfinite(secs))
101
+ sp_raise_cls("FloatDomainError", isnan(secs) ? "NaN" : (secs < 0 ? "-Infinity" : "Infinity"));
102
+ int e;
103
+ double m = frexp(secs, &e);
104
+ int64_t mi = (int64_t)(m * 9007199254740992.0); /* m * 2^53, exact */
105
+ e -= 53;
106
+ #if SP_HAVE_INT128
107
+ sp_int128 ns = (sp_int128)mi * 1000000000;
108
+ if (e > 0) ns = (e > 34) ? (ns < 0 ? INT64_MIN : INT64_MAX) : ns << e;
109
+ /* Floor toward -inf (arithmetic shift), matching CRuby's #nsec/#usec, which
110
+ truncate the exact rational. spinel stores nanosecond resolution, so the
111
+ sub-nanosecond bits CRuby keeps for #to_f round-tripping are lost by
112
+ design (see docs/limitations.md). */
113
+ else if (e < 0) ns = (-e > 126) ? (ns < 0 ? -1 : 0) : ns >> -e;
114
+ sp_int128 total = ((sp_int128)base_sec * 1000000000 + base_ns) + ns;
115
+ int64_t sec = (int64_t)(total / 1000000000);
116
+ int64_t rem = (int64_t)(total % 1000000000);
117
+ #else
118
+ /* no 128-bit integer (a 32-bit build): the same in long double, whose
119
+ 64-bit mantissa on x86 holds the nanosecond count of any date a
120
+ 32-bit sp_int can express; the floor is explicit */
121
+ long double nsd = ldexpl((long double)mi * 1000000000.0L, e);
122
+ long double totd = ((long double)base_sec * 1000000000.0L + (long double)base_ns) + floorl(nsd);
123
+ int64_t sec = (int64_t)floorl(totd / 1000000000.0L);
124
+ int64_t rem = (int64_t)(totd - (long double)sec * 1000000000.0L);
125
+ #endif
126
+ if (rem < 0) { sec -= 1; rem += 1000000000; }
127
+ *out_sec = sec;
128
+ *out_ns = (int32_t)rem;
129
+ }
130
+
131
+ /* POSIX convention: keep tv_nsec in [0, 1e9). For negative epoch with
132
+ a non-integer fractional part, decrement tv_sec and roll the fraction
133
+ into the positive nsec range -- so Time.at(-0.5).to_i returns -1, not 0. */
134
+ sp_Time sp_time_at_float(double epoch) {
135
+ sp_Time r = { 0, 0, 0 };
136
+ sp_time_shift_ns(epoch, 0, 0, &r.tv_sec, &r.tv_nsec);
137
+ return r;
138
+ }
139
+
140
+ /* Time.new(y[,mo[,d[,h[,mi[,s]]]]]) -- local construction. mktime
141
+ interprets the broken-down value in the host local zone and resolves
142
+ DST itself (tm_isdst=-1). The fixed-offset 7-arg form is a separate
143
+ issue. */
144
+ /* CRuby validates each civil component against a fixed range before
145
+ normalizing overflow into neighbouring fields (e.g. Feb 30 -> Mar 1 is
146
+ allowed, but mon 13 / mday 32 / hour 25 raise ArgumentError) (#3099). */
147
+ static void sp_time_check_args(int64_t mo, int64_t d, int64_t h, int64_t mi, int64_t s) {
148
+ if (mo < 1 || mo > 12) sp_raise_cls("ArgumentError", "mon out of range");
149
+ if (d < 1 || d > 31) sp_raise_cls("ArgumentError", "mday out of range");
150
+ if (h < 0 || h > 24) sp_raise_cls("ArgumentError", "hour out of range");
151
+ if (mi < 0 || mi > 59) sp_raise_cls("ArgumentError", "min out of range");
152
+ if (s < 0 || s > 60) sp_raise_cls("ArgumentError", "sec out of range");
153
+ }
154
+
155
+ static int64_t sp_time_civil_epoch(int64_t y, int64_t mo, int64_t d,
156
+ int64_t h, int64_t mi, int64_t s);
157
+
158
+ /* localtime/gmtime may share a process-wide buffer. Copying their result
159
+ still races with another OS worker; resolve into caller-owned storage.
160
+ Unlike localtime, POSIX localtime_r need not act as if it called tzset. */
161
+ static struct tm *sp_time_local_tm(time_t s, struct tm *bd) {
162
+ #ifndef __wasi__ /* WASI has no time zone database and no tzset */
163
+ tzset();
164
+ #endif
165
+ return localtime_r(&s, bd);
166
+ }
167
+
168
+ /* Not mktime(gmtime(s)) - s: macOS mktime answers -1 for any year before 1900. */
169
+ static int32_t sp_time_local_offset(time_t s) {
170
+ struct tm local;
171
+ struct tm *l = sp_time_local_tm(s, &local);
172
+ if (!l) return 0;
173
+ return (int32_t)(sp_time_civil_epoch(l->tm_year + 1900, l->tm_mon + 1, l->tm_mday,
174
+ l->tm_hour, l->tm_min, l->tm_sec) - (int64_t)s);
175
+ }
176
+
177
+ sp_Time sp_time_new(int64_t y, int64_t mo, int64_t d,
178
+ int64_t h, int64_t mi, int64_t s) {
179
+ sp_time_check_args(mo, d, h, mi, s);
180
+ struct tm tm;
181
+ memset(&tm, 0, sizeof(tm));
182
+ tm.tm_year = (int)y - 1900;
183
+ tm.tm_mon = (int)mo - 1;
184
+ tm.tm_mday = (int)d;
185
+ tm.tm_hour = (int)h;
186
+ tm.tm_min = (int)mi;
187
+ tm.tm_sec = (int)s;
188
+ tm.tm_isdst = -1;
189
+ tm.tm_wday = -1;
190
+ time_t e = mktime(&tm);
191
+ if (e == (time_t)-1 && tm.tm_wday == -1) {
192
+ /* Not the -1 epoch: mktime refused the year (macOS, before 1900); resolve via localtime, twice to settle an offset change. */
193
+ int64_t guess = sp_time_civil_epoch(y, mo, d, h, mi, s);
194
+ int64_t epoch = guess - sp_time_local_offset((time_t)guess);
195
+ epoch = guess - sp_time_local_offset((time_t)epoch);
196
+ return (sp_Time){ epoch, 0, 0 };
197
+ }
198
+ return (sp_Time){ (int64_t)e, 0, 0 };
199
+ }
200
+
201
+ /* Civil (y, mo, d, h, mi, s) -> UTC epoch seconds. Avoid timegm
202
+ (not portable; absent on MSVCRT). Compute the epoch via Howard
203
+ Hinnant's days_from_civil + a manual hour/minute/second add. */
204
+ static int64_t sp_time_civil_epoch(int64_t y, int64_t mo, int64_t d,
205
+ int64_t h, int64_t mi, int64_t s) {
206
+ int64_t yy = y - (mo <= 2 ? 1 : 0);
207
+ int64_t era = (yy >= 0 ? yy : yy - 399) / 400;
208
+ int64_t yoe = yy - era * 400;
209
+ int64_t doy = (153 * (mo + (mo > 2 ? -3 : 9)) + 2) / 5 + d - 1;
210
+ int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
211
+ int64_t days = era * 146097 + doe - 719468;
212
+ return days * 86400 + h * 3600 + mi * 60 + s;
213
+ }
214
+
215
+ /* Time.utc(y, m, d, h, mi, s) -- UTC construction. */
216
+ /* The month argument of the civil constructors accepts an English month name
217
+ as well as a number: Time.utc(2020, "feb", 4). A plain strtoll read those as
218
+ zero and the constructor rejected them (#3703). */
219
+ int64_t sp_time_month_arg(const char *s) {
220
+ static const char *const names[12] = {
221
+ "jan", "feb", "mar", "apr", "may", "jun",
222
+ "jul", "aug", "sep", "oct", "nov", "dec" };
223
+ if (!s) return 0;
224
+ if (strlen(s) == 3) {
225
+ char pre[4];
226
+ for (int i = 0; i < 3; i++) {
227
+ char ch = s[i];
228
+ pre[i] = (ch >= 'A' && ch <= 'Z') ? (char)(ch - 'A' + 'a') : ch;
229
+ }
230
+ pre[3] = '\0';
231
+ for (int m = 0; m < 12; m++)
232
+ if (strcmp(pre, names[m]) == 0) return (int64_t)(m + 1);
233
+ }
234
+ /* anything else is read as an Integer, and rejected the way Integer() would
235
+ be: CRuby takes only the three-letter abbreviations by name */
236
+ return (int64_t)sp_str_to_i_strict(s);
237
+ }
238
+
239
+ sp_Time sp_time_new_utc(int64_t y, int64_t mo, int64_t d,
240
+ int64_t h, int64_t mi, int64_t s) {
241
+ sp_time_check_args(mo, d, h, mi, s);
242
+ return (sp_Time){ sp_time_civil_epoch(y, mo, d, h, mi, s), 0, 1 };
243
+ }
244
+
245
+ /* A zone argument (`in:`, Time.new's 7th positional, or localtime /
246
+ getlocal's String): CRuby's utc_offset_arg. "UTC" in any case and "Z" are
247
+ UTC; a military letter is a whole-hour offset ("A".."I" +1..+9, "K".."M"
248
+ +10..+12, "N".."Y" -1..-12, no "J"); otherwise "+HH", "+HHMM", "+HHMMSS",
249
+ "+HH:MM" or "+HH:MM:SS", minutes and seconds below 60, where a negative
250
+ zero ("-00:00") is UTC as well. Any other spelling is CRuby's ArgumentError
251
+ naming the forms, and an offset of a day or more its "utc_offset out of
252
+ range" (#3696, #3697, #3698). */
253
+ int64_t sp_time_zone_arg_off(const char *z, int *is_utc_out) {
254
+ *is_utc_out = 0;
255
+ if (!z) sp_raise_cls("ArgumentError", "invalid time zone");
256
+ size_t len = strlen(z);
257
+ const char *min = NULL, *sec = NULL;
258
+ int64_t n = 0;
259
+ switch (len) {
260
+ case 1:
261
+ if (z[0] == 'Z') { *is_utc_out = 1; return 0; }
262
+ if (z[0] >= 'A' && z[0] <= 'I') return (int64_t)(z[0] - 'A' + 1) * 3600;
263
+ if (z[0] >= 'K' && z[0] <= 'M') return (int64_t)(z[0] - 'A') * 3600;
264
+ if (z[0] >= 'N' && z[0] <= 'Y') return (int64_t)('M' - z[0]) * 3600;
265
+ goto invalid;
266
+ case 3:
267
+ if ((z[0] | 0x20) == 'u' && (z[1] | 0x20) == 't' && (z[2] | 0x20) == 'c') { *is_utc_out = 1; return 0; }
268
+ break; /* "+HH" */
269
+ case 9: if (z[6] != ':') goto invalid; sec = z + 7; /* fall through: "+HH:MM:SS" */
270
+ case 6: if (z[3] != ':') goto invalid; min = z + 4; break; /* "+HH:MM" */
271
+ case 7: sec = z + 5; /* fall through: "+HHMMSS" */
272
+ case 5: min = z + 3; break; /* "+HHMM" */
273
+ default: goto invalid;
274
+ }
275
+ if (sec) {
276
+ if (!isdigit((unsigned char)sec[0]) || !isdigit((unsigned char)sec[1]) || sec[0] > '5') goto invalid;
277
+ n += (sec[0] - '0') * 10 + (sec[1] - '0');
278
+ }
279
+ if (min) {
280
+ if (!isdigit((unsigned char)min[0]) || !isdigit((unsigned char)min[1]) || min[0] > '5') goto invalid;
281
+ n += ((min[0] - '0') * 10 + (min[1] - '0')) * 60;
282
+ }
283
+ if ((z[0] != '+' && z[0] != '-') || !isdigit((unsigned char)z[1]) || !isdigit((unsigned char)z[2])) goto invalid;
284
+ n += (int64_t)((z[1] - '0') * 10 + (z[2] - '0')) * 3600;
285
+ if (z[0] == '-') {
286
+ if (n == 0) { *is_utc_out = 1; return 0; }
287
+ n = -n;
288
+ }
289
+ if (n <= -86400 || n >= 86400) sp_raise_cls("ArgumentError", "utc_offset out of range");
290
+ return n;
291
+ invalid:
292
+ sp_raise_cls("ArgumentError", sp_sprintf("\"+HH:MM\", \"-HH:MM\", \"UTC\" or \"A\"..\"I\",\"K\"..\"Z\" expected for utc_offset: %s", z));
293
+ return 0;
294
+ }
295
+
296
+ /* Re-read a Time in the zone the argument names (#3698). */
297
+ sp_Time sp_time_in_zone_i(sp_Time t, int64_t off) {
298
+ if (off <= -86400 || off >= 86400)
299
+ sp_raise_cls("ArgumentError", "utc_offset out of range");
300
+ t.is_utc = 2; t.utc_off = (int32_t)off;
301
+ return t;
302
+ }
303
+ sp_Time sp_time_in_zone_s(sp_Time t, const char *z) {
304
+ int isu = 0;
305
+ int64_t off = sp_time_zone_arg_off(z, &isu);
306
+ if (isu) { t.is_utc = 1; t.utc_off = 0; }
307
+ else { t.is_utc = 2; t.utc_off = (int32_t)off; }
308
+ return t;
309
+ }
310
+
311
+ /* Time.new(y, mo, d, h, mi, s, utc_offset) -- the civil value is read in a
312
+ fixed zone off seconds east of UTC, so the epoch is the UTC epoch of the
313
+ same civil value minus that offset. CRuby bounds the offset to a day. */
314
+ sp_Time sp_time_new_off(int64_t y, int64_t mo, int64_t d,
315
+ int64_t h, int64_t mi, int64_t s, int64_t off) {
316
+ if (off <= -86400 || off >= 86400)
317
+ sp_raise_cls("ArgumentError", "utc_offset out of range");
318
+ sp_time_check_args(mo, d, h, mi, s);
319
+ sp_Time t = { sp_time_civil_epoch(y, mo, d, h, mi, s) - off, 0, 2 };
320
+ t.utc_off = (int32_t)off;
321
+ return t;
322
+ }
323
+
324
+ /* Time.utc/local(y, mo, d, h, mi, s, usec) -- the 7th positional argument is
325
+ microseconds of second. */
326
+ sp_Time sp_time_with_usec(sp_Time t, int64_t usec) {
327
+ if (usec < 0 || usec >= 1000000)
328
+ sp_raise_cls("ArgumentError", "subsecx out of range");
329
+ t.tv_nsec = (int32_t)(usec * 1000);
330
+ return t;
331
+ }
332
+ /* a Float microsecond argument carries into the nanosecond field (#3092). */
333
+ sp_Time sp_time_with_usec_f(sp_Time t, double usec) {
334
+ if (usec < 0 || usec >= 1000000)
335
+ sp_raise_cls("ArgumentError", "subsecx out of range");
336
+ t.tv_nsec = (int32_t)(usec * 1000.0);
337
+ return t;
338
+ }
339
+
340
+ /* Time.new(String): the fixed CRuby form "YYYY-MM-DD HH:MM:SS[.frac]" with
341
+ an optional " +HH:MM" / " -HH:MM" / " UTC" zone suffix. A date without a
342
+ time and any other shape raise CRuby's ArgumentError messages; anything
343
+ the grammar does not cover must be loud, never a guessed instant. */
344
+ sp_Time sp_time_parse(const char *s) {SP_GC_ROOT_STR(s);
345
+ const char *sp_sprintf(const char *fmt, ...); /* generated TU */
346
+ int y, mo, d, h, mi, sec, n = 0;
347
+ /* a year alone ("2021", "-44", "+12345") is Time.new(year), as CRuby
348
+ reads it: four digits at least, local midnight of January 1 */
349
+ { const char *q = s + (*s == '+' || *s == '-');
350
+ size_t nd = 0;
351
+ while (q[nd] >= '0' && q[nd] <= '9') nd++;
352
+ if (nd > 0 && q[nd] == 0) {
353
+ if (nd < 4) sp_raise_cls("ArgumentError", sp_sprintf("year must be 4 or more digits: %s", s));
354
+ int64_t yy = 0;
355
+ for (size_t i = 0; i < nd && i < 18; i++) yy = yy * 10 + (q[i] - '0');
356
+ if (*s == '-') yy = -yy;
357
+ return sp_time_new(yy, 1, 1, 0, 0, 0);
358
+ }
359
+ }
360
+ if (sscanf(s, "%4d-%2d-%2d%n", &y, &mo, &d, &n) != 3 || n == 0)
361
+ sp_raise_cls("ArgumentError", sp_sprintf("can't parse: \"%s\"", s));
362
+ const char *p = s + n;
363
+ if (*p == 0)
364
+ sp_raise_cls("ArgumentError", "no time information");
365
+ n = 0;
366
+ if (sscanf(p, " %2d:%2d:%2d%n", &h, &mi, &sec, &n) != 3 || n == 0)
367
+ sp_raise_cls("ArgumentError", sp_sprintf("can't parse: \"%s\"", s));
368
+ p += n;
369
+ int32_t nsec = 0;
370
+ if (*p == '.') {
371
+ p++;
372
+ int digits = 0;
373
+ int64_t frac = 0;
374
+ while (*p >= '0' && *p <= '9' && digits < 9) { frac = frac * 10 + (*p - '0'); p++; digits++; }
375
+ if (digits == 0)
376
+ sp_raise_cls("ArgumentError", sp_sprintf("can't parse: \"%s\"", s));
377
+ while (*p >= '0' && *p <= '9') p++; /* sub-ns digits are beyond sp_Time */
378
+ while (digits < 9) { frac *= 10; digits++; }
379
+ nsec = (int32_t)frac;
380
+ }
381
+ while (*p == ' ') p++;
382
+ if (*p == 0) {
383
+ /* no zone: host-local, like the civil Time.new */
384
+ sp_Time t = sp_time_new(y, mo, d, h, mi, sec);
385
+ t.tv_nsec = nsec;
386
+ return t;
387
+ }
388
+ if (strcmp(p, "UTC") == 0 || strcmp(p, "Z") == 0) {
389
+ sp_Time t = sp_time_new_utc(y, mo, d, h, mi, sec);
390
+ t.tv_nsec = nsec;
391
+ return t;
392
+ }
393
+ int oh, om;
394
+ n = 0;
395
+ if ((*p == '+' || *p == '-') && sscanf(p + 1, "%2d:%2d%n", &oh, &om, &n) == 2 &&
396
+ n > 0 && p[1 + n] == 0) {
397
+ int64_t off = (int64_t)oh * 3600 + (int64_t)om * 60;
398
+ if (*p == '-') off = -off;
399
+ sp_Time t = sp_time_new_off(y, mo, d, h, mi, sec, off);
400
+ t.tv_nsec = nsec;
401
+ return t;
402
+ }
403
+ sp_raise_cls("ArgumentError", sp_sprintf("can't parse: \"%s\"", s));
404
+ }
405
+
406
+ sp_Time sp_time_utc(sp_Time t) {
407
+ t.is_utc = 1;
408
+ return t;
409
+ }
410
+
411
+ sp_Time sp_time_localtime(sp_Time t) {
412
+ t.is_utc = 0;
413
+ return t;
414
+ }
415
+ /* Time#getlocal(off)/#localtime(off): reinterpret the instant in a fixed zone
416
+ `off` seconds east of UTC, without changing the underlying epoch (#3093). */
417
+ sp_Time sp_time_getlocal_off(sp_Time t, int64_t off) {
418
+ if (off <= -86400 || off >= 86400)
419
+ sp_raise_cls("ArgumentError", "utc_offset out of range");
420
+ t.is_utc = 2;
421
+ t.utc_off = (int32_t)off;
422
+ return t;
423
+ }
424
+
425
+ /* is_utc selects gmtime vs localtime, off is UTC offset in seconds,
426
+ zbuf is the timezone abbreviation (8 bytes). The offset is computed
427
+ (sp_time_local_offset), not read from %z: MSVCRT's %z emits the
428
+ timezone name, not ±HHMM. */
429
+ void sp_time_vtm(sp_Time t, struct tm *bd, int32_t *off, char *zbuf) {
430
+ time_t s = (time_t)t.tv_sec;
431
+ if (t.is_utc == 2) {
432
+ /* fixed offset: the civil value is the UTC civil value shifted east */
433
+ time_t sh = s + (time_t)t.utc_off;
434
+ if (!gmtime_r(&sh, bd)) memset(bd, 0, sizeof(*bd));
435
+ if (off) *off = t.utc_off;
436
+ if (zbuf) zbuf[0] = 0;
437
+ }
438
+ else if (t.is_utc) {
439
+ if (!gmtime_r(&s, bd)) memset(bd, 0, sizeof(*bd));
440
+ if (off) *off = 0;
441
+ if (zbuf) { zbuf[0]='U'; zbuf[1]='T'; zbuf[2]='C'; zbuf[3]=0; }
442
+ }
443
+ else {
444
+ if (!sp_time_local_tm(s, bd)) memset(bd, 0, sizeof(*bd));
445
+ if (off) *off = sp_time_local_offset(s);
446
+ if (zbuf) {
447
+ if (strftime(zbuf, 8, "%Z", bd) == 0) zbuf[0] = 0;
448
+ }
449
+ }
450
+ }
451
+
452
+ int64_t sp_time_year(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)(b.tm_year+1900);}
453
+ int64_t sp_time_mon(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)(b.tm_mon+1);}
454
+ int64_t sp_time_mday(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)b.tm_mday;}
455
+ int64_t sp_time_hour(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)b.tm_hour;}
456
+ int64_t sp_time_min(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)b.tm_min;}
457
+ int64_t sp_time_sec(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)b.tm_sec;}
458
+ int64_t sp_time_wday(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)b.tm_wday;}
459
+ int64_t sp_time_yday(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)(b.tm_yday+1);}
460
+ int64_t sp_time_isdst(sp_Time t){struct tm b;sp_time_vtm(t,&b,NULL,NULL);return (int64_t)(b.tm_isdst>0?1:0);}
461
+ int64_t sp_time_utc_offset(sp_Time t){int32_t o;struct tm b;sp_time_vtm(t,&b,&o,NULL);return (int64_t)o;}
462
+
463
+ /* Time + Numeric / Time - Numeric. secs may be fractional; the shift is
464
+ exact in the double's binary value (see sp_time_shift_ns). The zone kind
465
+ and offset are inherited from the receiver. */
466
+ sp_Time sp_time_add(sp_Time t, double secs) {
467
+ sp_Time r = t;
468
+ sp_time_shift_ns(secs, t.tv_sec, t.tv_nsec, &r.tv_sec, &r.tv_nsec);
469
+ return r;
470
+ }
471
+
472
+ /* strftime returns 0 -- never overruns the buffer -- when the formatted
473
+ result would exceed it, which we surface as "". The 4 KB buffer covers
474
+ any realistic format (CRuby's built-ins are ~25 bytes; this leaves room
475
+ for long literal text or wide fields). A pathological field width
476
+ (`"%1000000000F"`, which CRuby rejects with ERANGE) does not fit and
477
+ yields "" -- a graceful empty string rather than a crash. */
478
+ /* The UTC offset (seconds) of a Time, mirroring sp_time_iso8601's manual calc. */
479
+ static long sp_time_offset_sec(sp_Time t) {
480
+ if (t.is_utc == 2) return t.utc_off; /* fixed offset (Time.at in:) */
481
+ if (t.is_utc) return 0;
482
+ return (long)sp_time_local_offset((time_t)t.tv_sec);
483
+ }
484
+
485
+ /* A year as Ruby writes it: zero-padded to four digits with the sign in
486
+ front ("0012", "-0012", "10000"); a positive `width` is the whole field,
487
+ sign included, as in "%4Y" ("-012"). C's %Y is not portable here: glibc
488
+ does not pad it at all, and macOS pads a negative year inside the sign
489
+ ("-012" for "-0012"). Returns the length written, as snprintf does. */
490
+ static int sp_time_year_field(char *buf, size_t cap, long yr, int width) {
491
+ const char *sign = yr < 0 ? "-" : "";
492
+ unsigned long mag = yr < 0 ? 0UL - (unsigned long)yr : (unsigned long)yr;
493
+ int digits = width > 0 ? width - (yr < 0) : 4;
494
+ return snprintf(buf, cap, "%s%0*lu", sign, digits > 0 ? digits : 1, mag);
495
+ }
496
+
497
+ static int sp_time_year_str(char *buf, size_t cap, long yr) {
498
+ return sp_time_year_field(buf, cap, yr, -1);
499
+ }
500
+
501
+ /* buf <- the year, then C strftime of `rest` (fields that carry no year).
502
+ Returns the length, or 0 if it did not fit, as strftime does. */
503
+ static size_t sp_time_year_then(char *buf, size_t cap, const struct tm *b, const char *rest) {
504
+ int yn = sp_time_year_str(buf, cap, (long)b->tm_year + 1900);
505
+ if (yn < 0 || (size_t)yn >= cap) return 0;
506
+ if (!*rest) return (size_t)yn;
507
+ size_t r = strftime(buf + yn, cap - (size_t)yn, rest, b);
508
+ return r ? (size_t)yn + r : 0;
509
+ }
510
+
511
+ /* Ruby-compatible strftime: C strftime handles the standard directives, but
512
+ Ruby adds %L/%N (subsec), %s (epoch, which C's %s would take through a LOCAL
513
+ mktime), %P (lowercase am/pm), the %:z/%::z colon offsets, and width/flag
514
+ modifiers (%3S, %6N, %10Y). Walk the format, compute those directly, and
515
+ pad; delegate a bare standard directive to strftime. (#2635, #2636) */
516
+ const char *sp_time_strftime(sp_Time t, const char *fmt) {SP_GC_ROOT_STR(fmt);
517
+ /* is_utc is a 3-state kind, not a flag: kind 2 is a fixed offset whose
518
+ civil fields are the UTC ones shifted by utc_off. Reading it as a
519
+ boolean sent kind 2 down the gmtime branch, so every field directive
520
+ rendered UTC under a %z that correctly said otherwise. sp_time_vtm
521
+ resolves all three kinds. */
522
+ struct tm tmv;
523
+ sp_time_vtm(t, &tmv, NULL, NULL);
524
+ static char out[8192];
525
+ size_t oi = 0;
526
+ for (const char *p = fmt; *p && oi < sizeof(out) - 128; p++) {
527
+ if (*p != '%') { out[oi++] = *p; continue; }
528
+ const char *tok = p++;
529
+ int upcase = 0, downcase = 0, pad0 = 0, padsp = 0, nopad = 0, colon = 0;
530
+ for (;; p++) {
531
+ if (*p == '^') upcase = 1; else if (*p == '#') downcase = 1;
532
+ else if (*p == '0') pad0 = 1; else if (*p == '_') padsp = 1;
533
+ else if (*p == '-') nopad = 1; else break;
534
+ }
535
+ while (*p == ':') { colon++; p++; }
536
+ int width = -1;
537
+ if (*p >= '0' && *p <= '9') { width = 0; while (*p >= '0' && *p <= '9') width = width * 10 + (*p++ - '0'); }
538
+ /* the E / O locale modifiers select an alternative representation the C
539
+ locale does not have, so the unmodified directive is what they mean (#3705) */
540
+ while ((*p == 'E' || *p == 'O') && p[1]) p++;
541
+ char d = *p;
542
+ /* a format ending in a bare `%` is invalid, not a literal one (#3705) */
543
+ if (!d) sp_raise_cls("ArgumentError", "invalid format");
544
+ char val[128]; val[0] = 0;
545
+ if (d == '%') { val[0] = '%'; val[1] = 0; }
546
+ else if (d == 's') snprintf(val, sizeof val, "%lld", (long long)t.tv_sec);
547
+ else if (d == 'L') {
548
+ /* a width on %L asks for that many fractional digits, not left padding
549
+ of the millisecond count (#3705) */
550
+ int lw = width > 0 ? width : 3;
551
+ char nb[24]; snprintf(nb, sizeof nb, "%09ld", (long)t.tv_nsec);
552
+ if (lw <= 9) { memcpy(val, nb, (size_t)lw); val[lw] = 0; }
553
+ else { strcpy(val, nb); for (int i = 9; i < lw && i < 120; i++) val[i] = '0'; val[lw < 120 ? lw : 120] = 0; }
554
+ width = -1;
555
+ }
556
+ else if (d == 'N') {
557
+ int w = width > 0 ? width : 9;
558
+ char nb[16]; snprintf(nb, sizeof nb, "%09ld", (long)t.tv_nsec);
559
+ if (w <= 9) { memcpy(val, nb, (size_t)w); val[w] = 0; }
560
+ else { strcpy(val, nb); for (int i = 9; i < w && i < 120; i++) val[i] = '0'; val[w < 120 ? w : 120] = 0; }
561
+ width = -1; /* width consumed by the subsecond precision */
562
+ }
563
+ else if (d == 'z') {
564
+ /* bare %z computed from the receiver's own offset, never C strftime's
565
+ tm_gmtoff -- that field is filled from the tm's construction path and
566
+ varies by platform, and a fixed-offset time (is_utc == 2) has no tm
567
+ representation at all (#2635) */
568
+ if (!colon) {
569
+ long off0 = sp_time_offset_sec(t);
570
+ char sign0 = off0 < 0 ? '-' : '+'; long a0 = off0 < 0 ? -off0 : off0;
571
+ snprintf(val, sizeof val, "%c%02d%02d", sign0, (int)(a0 / 3600), (int)((a0 / 60) % 60));
572
+ }
573
+ else {
574
+ long off = sp_time_offset_sec(t);
575
+ char sign = off < 0 ? '-' : '+'; long a = off < 0 ? -off : off;
576
+ int oh = (int)(a / 3600), om = (int)((a / 60) % 60), os = (int)(a % 60);
577
+ /* %:::z collapses to the minimal precision that loses nothing */
578
+ if (colon >= 3) {
579
+ if (os) snprintf(val, sizeof val, "%c%02d:%02d:%02d", sign, oh, om, os);
580
+ else if (om) snprintf(val, sizeof val, "%c%02d:%02d", sign, oh, om);
581
+ else snprintf(val, sizeof val, "%c%02d", sign, oh);
582
+ }
583
+ else if (colon == 2) snprintf(val, sizeof val, "%c%02d:%02d:%02d", sign, oh, om, os);
584
+ else snprintf(val, sizeof val, "%c%02d:%02d", sign, oh, om);
585
+ }
586
+ }
587
+ else if (d == 'P') { char b2[16]; strftime(b2, sizeof b2, "%p", &tmv); for (char *q = b2; *q; q++) *q = (char)tolower((unsigned char)*q); strcpy(val, b2); }
588
+ /* CRuby names a UTC time "UTC", not the C locale's "GMT"; a fixed-offset
589
+ time has no zone NAME at all, so %Z is empty there (Time#zone is nil). */
590
+ else if (d == 'Z' && t.is_utc) { if (t.is_utc == 1) strcpy(val, "UTC"); else val[0] = 0; }
591
+ /* Ruby's %Y is zero-padded to four digits; C's is not, so a year below
592
+ 1000 came out "1" where CRuby writes "0001". Ruby keeps the sign
593
+ outside the padding, so -1 is "-0001". The directives that contain
594
+ the year (%F, %c, %v) take it from the same place, and the century
595
+ and two-digit year round toward minus infinity, as Integer#div and
596
+ #% do: -12 is century -1, year 88. */
597
+ else if (d == 'Y' || d == 'G') {
598
+ long yr = (long)tmv.tm_year + 1900;
599
+ if (d == 'G') {
600
+ /* the ISO 8601 week-based year: C computes it, Ruby formats it */
601
+ char gb[32];
602
+ if (strftime(gb, sizeof gb, "%G", &tmv)) yr = strtol(gb, NULL, 10);
603
+ }
604
+ /* the bare directive pads to four digits; a width, `_` or `-` takes
605
+ the signed number and the padding below sizes it (the sign counts
606
+ toward the width, and zeros go after it) */
607
+ if (width > 0 || padsp || nopad) {
608
+ snprintf(val, sizeof val, "%ld", yr);
609
+ if (width <= 0 && padsp) width = 4 + (yr < 0);
610
+ }
611
+ else sp_time_year_str(val, sizeof val, yr);
612
+ }
613
+ else if (d == 'F') sp_time_year_then(val, sizeof val, &tmv, "-%m-%d");
614
+ else if (d == 'C' || d == 'y' || d == 'x' || d == 'D') {
615
+ long yr = (long)tmv.tm_year + 1900;
616
+ long cen = yr >= 0 ? yr / 100 : -((-yr + 99) / 100);
617
+ long yy = yr - cen * 100;
618
+ if (d == 'C') snprintf(val, sizeof val, "%02ld", cen);
619
+ else if (d == 'y') snprintf(val, sizeof val, "%02ld", yy);
620
+ else snprintf(val, sizeof val, "%02d/%02d/%02ld", tmv.tm_mon + 1, tmv.tm_mday, yy);
621
+ }
622
+ else if (d == 'c') {
623
+ size_t n = strftime(val, sizeof val, "%a %b %e %H:%M:%S ", &tmv);
624
+ if (n) sp_time_year_str(val + n, sizeof val - n, (long)tmv.tm_year + 1900);
625
+ }
626
+ else if (d == 'v') {
627
+ /* "%e-%^b-%4Y": the year four wide, sign included */
628
+ size_t n = strftime(val, sizeof val, "%e-%b-", &tmv);
629
+ for (size_t k = 0; k < n; k++) val[k] = (char)toupper((unsigned char)val[k]);
630
+ if (n) sp_time_year_field(val + n, sizeof val - n, (long)tmv.tm_year + 1900, 4);
631
+ }
632
+ else if (d == 'g') {
633
+ /* the ISO week-based year's last two digits, rounded as %y is */
634
+ char gb[32];
635
+ long gy = strftime(gb, sizeof gb, "%G", &tmv) ? strtol(gb, NULL, 10) : (long)tmv.tm_year + 1900;
636
+ long gc = gy >= 0 ? gy / 100 : -((-gy + 99) / 100);
637
+ snprintf(val, sizeof val, "%02ld", gy - gc * 100);
638
+ }
639
+ else if (strchr("aAbBcCdDeFgGhHIjklmMnprRSTtuUvVwWxXyYzZ", d)) {
640
+ /* a standard Ruby directive: format the bare `%X` (we redo width/case
641
+ ourselves for portability) */
642
+ char f2[3] = { '%', d, 0 };
643
+ strftime(val, sizeof val, f2, &tmv);
644
+ }
645
+ else {
646
+ /* not a Ruby Time#strftime directive (e.g. `%+`): emit the token
647
+ verbatim -- CRuby does, and a platform's C strftime must not interpret
648
+ it (macOS treats `%+` as date(1), glibc leaves it literal). */
649
+ size_t tl = (size_t)(p - tok) + 1;
650
+ if (tl < sizeof val) { memcpy(val, tok, tl); val[tl] = 0; }
651
+ width = -1; upcase = downcase = 0;
652
+ }
653
+ if (upcase) for (char *q = val; *q; q++) *q = (char)toupper((unsigned char)*q);
654
+ /* `%#` changes the field's case as a WHOLE: all-uppercase becomes
655
+ lowercase, anything else becomes uppercase. Per-character swapcase
656
+ instead turned "January" into "jANUARY" where CRuby answers
657
+ "JANUARY" -- only fields that are already uppercase (%p, %Z) agreed. */
658
+ if (downcase) {
659
+ int has_lower = 0;
660
+ for (char *q = val; *q; q++) if (islower((unsigned char)*q)) { has_lower = 1; break; }
661
+ for (char *q = val; *q; q++)
662
+ *q = has_lower ? (char)toupper((unsigned char)*q) : (char)tolower((unsigned char)*q);
663
+ }
664
+ /* the `-` (no-pad) and `_` (space-pad) modifiers rework the default zero
665
+ padding that C strftime already applied to a numeric field (#3090) */
666
+ /* the space-padded fields (%e / %k / %l) take `-` (strip) and `0` (zero
667
+ pad) the same way the zero-padded ones take `-` and `_` (#3705) */
668
+ if ((nopad || pad0) && val[0] == ' ') {
669
+ size_t sp0 = 0; while (val[sp0] == ' ' && val[sp0 + 1] != 0) sp0++;
670
+ if (nopad) memmove(val, val + sp0, strlen(val) - sp0 + 1);
671
+ else for (size_t k = 0; k < sp0; k++) val[k] = '0';
672
+ }
673
+ if ((nopad || padsp) && val[0]) {
674
+ int all_digit = 1;
675
+ for (char *q = val; *q; q++) if (!isdigit((unsigned char)*q)) { all_digit = 0; break; }
676
+ if (all_digit) {
677
+ size_t z = 0; while (val[z] == '0' && val[z + 1] != 0) z++; /* keep the last digit */
678
+ if (nopad) memmove(val, val + z, strlen(val) - z + 1);
679
+ else for (size_t k = 0; k < z; k++) val[k] = ' ';
680
+ }
681
+ }
682
+ size_t vl = strlen(val), v0 = 0;
683
+ if (width > 0 && !nopad && vl < (size_t)width) {
684
+ /* CRuby pads a width with zeros for a number and with spaces for
685
+ text (%A %a %B %b %h %p %P %Z), for the composites (%c %x %X %D %F
686
+ %T %R %r %v), for the blank-padded numbers (%e %k %l) and for %n
687
+ %t %%; a `0` or `_` flag picks the character either way. Every
688
+ directive took zeros, so "%10A" was "000Tuesday". */
689
+ char pc = padsp ? ' ' : pad0 ? '0' : (strchr("AaBbhpPZcxXDFTRrvektlnt%", d) ? ' ' : '0');
690
+ /* zeros go after a sign, as CRuby pads "%10s" of -5 to "-000000005"
691
+ and "%10z" to "+000000000"; spaces go before it */
692
+ if (pc == '0' && (val[0] == '-' || val[0] == '+') && oi < sizeof(out) - 2) { out[oi++] = val[0]; v0 = 1; }
693
+ for (size_t k = vl; k < (size_t)width && oi < sizeof(out) - 2; k++) out[oi++] = pc;
694
+ }
695
+ (void)tok;
696
+ for (size_t k = v0; k < vl && oi < sizeof(out) - 2; k++) out[oi++] = val[k];
697
+ }
698
+ out[oi] = 0;
699
+ return sp_str_dup_external(out);
700
+ }
701
+
702
+ /* RFC 3339 zone suffix: "Z" for a UTC time, "+HH:MM" otherwise. `off` is
703
+ the receiver's own offset as sp_time_vtm resolved it, so the fixed-offset
704
+ kind carries its utc_off here instead of being mistaken for UTC. Returns
705
+ the number of bytes appended (0 if they would not fit). */
706
+ static size_t sp_time_iso_zone(char *buf, size_t n, size_t cap, sp_Time t, int32_t off) {
707
+ if (t.is_utc == 1) {
708
+ if (n + 1 >= cap) return 0;
709
+ buf[n] = 'Z'; buf[n + 1] = 0;
710
+ return 1;
711
+ }
712
+ if (n + 6 >= cap) return 0;
713
+ char sign = off >= 0 ? '+' : '-';
714
+ long a = off < 0 ? -(long)off : (long)off;
715
+ int oh = (int)(a / 3600), om = (int)((a / 60) % 60);
716
+ buf[n++] = sign;
717
+ buf[n++] = (char)('0' + (oh / 10));
718
+ buf[n++] = (char)('0' + (oh % 10));
719
+ buf[n++] = ':';
720
+ buf[n++] = (char)('0' + (om / 10));
721
+ buf[n++] = (char)('0' + (om % 10));
722
+ buf[n] = 0;
723
+ return 6;
724
+ }
725
+
726
+ /* Time#httpdate (RFC 1123, always GMT) and Time#rfc2822, whose UTC time
727
+ is written -0000 as CRuby's time.rb does. */
728
+ const char *sp_time_httpdate(sp_Time t) {
729
+ return sp_time_strftime(sp_time_utc(t), "%a, %d %b %Y %H:%M:%S GMT");
730
+ }
731
+ const char *sp_time_rfc2822(sp_Time t) {
732
+ return sp_time_strftime(t, t.is_utc == 1 ? "%a, %d %b %Y %H:%M:%S -0000" : "%a, %d %b %Y %H:%M:%S %z");
733
+ }
734
+
735
+ /* RFC 3339 / iso8601. sp_time_vtm resolves the civil fields and the offset
736
+ for all three zone kinds; the suffix is formatted here because MSVCRT's
737
+ %z renders the timezone name rather than ±HHMM. */
738
+ const char *sp_time_iso8601(sp_Time t) {
739
+ char buf[64];
740
+ size_t cap = sizeof(buf);
741
+ struct tm b;
742
+ int32_t off;
743
+ sp_time_vtm(t, &b, &off, NULL);
744
+ size_t n = sp_time_year_then(buf, cap, &b, "-%m-%dT%H:%M:%S");
745
+ if (n == 0) return sp_str_empty;
746
+ sp_time_iso_zone(buf, n, cap, t, off);
747
+ return sp_str_dup_external(buf);
748
+ }
749
+
750
+ /* iso8601 / xmlschema with a fraction-digits argument: like sp_time_iso8601
751
+ but inserting `digits` truncated fractional-second places (#3094, #3095). */
752
+ const char *sp_time_iso8601_frac(sp_Time t, int64_t digits) {
753
+ if (digits <= 0) return sp_time_iso8601(t);
754
+ if (digits > 50) digits = 50;
755
+ char buf[128];
756
+ size_t cap = sizeof(buf);
757
+ struct tm b;
758
+ int32_t off;
759
+ sp_time_vtm(t, &b, &off, NULL);
760
+ size_t n = sp_time_year_then(buf, cap, &b, "-%m-%dT%H:%M:%S");
761
+ if (n == 0) return sp_str_empty;
762
+ char fb[16]; snprintf(fb, sizeof fb, "%09ld", (long)t.tv_nsec);
763
+ if (n + 1 + (size_t)digits < cap) {
764
+ buf[n++] = '.';
765
+ for (int64_t i = 0; i < digits; i++) buf[n++] = i < 9 ? fb[i] : '0';
766
+ buf[n] = 0;
767
+ }
768
+ sp_time_iso_zone(buf, n, cap, t, off);
769
+ return sp_str_dup_external(buf);
770
+ }
771
+
772
+ const char *sp_time_zone(sp_Time t) {
773
+ /* a fixed-offset Time has no zone NAME, and CRuby answers nil for it rather
774
+ than the empty string the broken-down form leaves behind (#3701) */
775
+ if (t.is_utc == 2) return NULL;
776
+ char buf[8];
777
+ struct tm b;
778
+ sp_time_vtm(t, &b, NULL, buf);
779
+ return sp_str_dup_external(buf);
780
+ }
781
+
782
+ /* Scalar Time inspect. CRuby form: local "YYYY-MM-DD HH:MM:SS +0900",
783
+ UTC "YYYY-MM-DD HH:MM:SS UTC". The poly-box path keeps its own
784
+ sp_Time_inspect; this value-taking variant is for the scalar
785
+ p/puts/to_s codegen path. */
786
+ static const char *sp_time_fmt(sp_Time t, int frac) {
787
+ char buf[64];
788
+ size_t cap = sizeof(buf);
789
+ struct tm b;
790
+ int32_t off;
791
+ sp_time_vtm(t, &b, &off, NULL);
792
+ /* the year by hand (sp_time_year_then): Time.utc(1,1,1).to_s is
793
+ "0001-01-01 ..." in CRuby, and C's %Y wrote "1-01-01 ..." */
794
+ size_t n = sp_time_year_then(buf, cap, &b, "-%m-%d %H:%M:%S");
795
+ if (n == 0) {
796
+ snprintf(buf, cap, "Time(%lld)", (long long)t.tv_sec);
797
+ return sp_str_dup_external(buf);
798
+ }
799
+ if (frac && t.tv_nsec != 0 && n + 11 < cap) {
800
+ /* fractional seconds, trailing zeros trimmed (".123456", ".5") */
801
+ n += (size_t)snprintf(buf + n, cap - n, ".%09d", (int)t.tv_nsec);
802
+ while (buf[n - 1] == '0') buf[--n] = 0;
803
+ }
804
+ if (n + 10 < cap) {
805
+ if (t.is_utc == 1) {
806
+ buf[n++]=' '; buf[n++]='U'; buf[n++]='T'; buf[n++]='C'; buf[n]=0;
807
+ }
808
+ else {
809
+ char sign = off >= 0 ? '+' : '-';
810
+ long a = off < 0 ? -(long)off : (long)off;
811
+ int oh = (int)(a / 3600);
812
+ int om = (int)((a / 60) % 60);
813
+ int os = (int)(a % 60);
814
+ buf[n++]=' '; buf[n++]=sign;
815
+ buf[n++]=(char)('0'+oh/10); buf[n++]=(char)('0'+oh%10);
816
+ buf[n++]=(char)('0'+om/10); buf[n++]=(char)('0'+om%10);
817
+ if (os) { /* CRuby renders a sub-minute offset as +HHMMSS */
818
+ buf[n++]=(char)('0'+os/10); buf[n++]=(char)('0'+os%10);
819
+ }
820
+ buf[n]=0;
821
+ }
822
+ }
823
+ return sp_str_dup_external(buf);
824
+ }
825
+
826
+ /* Time#inspect renders fractional seconds; Time#to_s does not. */
827
+ const char *sp_time_inspect_v(sp_Time t) { return sp_time_fmt(t, 1); }
828
+ const char *sp_time_to_s_v(sp_Time t) { return sp_time_fmt(t, 0); }
829
+
830
+ /* ---- comparison + shifts (moved from spinel_rt.h; cold) ---- */
831
+ int sp_time_cmp(sp_Time a, sp_Time b) {
832
+ if (a.tv_sec < b.tv_sec) return -1;
833
+ if (a.tv_sec > b.tv_sec) return 1;
834
+ if (a.tv_nsec < b.tv_nsec) return -1;
835
+ if (a.tv_nsec > b.tv_nsec) return 1;
836
+ return 0;
837
+ }
838
+ sp_Time sp_time_add_f(sp_Time t, double secs) {
839
+ return sp_time_add(t, secs);
840
+ }
841
+ /* Value hash: equal (tv_sec, tv_nsec) pairs must hash equal regardless of
842
+ zone kind, mirroring Time#== which compares the instant only. */
843
+ int64_t sp_time_hash(sp_Time t) {
844
+ uint64_t h = (uint64_t)t.tv_sec * 1000000007ULL + (uint64_t)(uint32_t)t.tv_nsec;
845
+ h ^= h >> 33;
846
+ return (int64_t)(h & 0x3fffffffffffffffULL);
847
+ }
848
+ sp_Time sp_time_add_i(sp_Time t, int64_t secs) {
849
+ sp_Time r = t;
850
+ r.tv_sec = t.tv_sec + (time_t)secs;
851
+ return r;
852
+ }
853
+ sp_Time sp_time_sub_i(sp_Time t, int64_t secs) {
854
+ sp_Time r = t;
855
+ r.tv_sec = t.tv_sec - (time_t)secs;
856
+ return r;
857
+ }
858
+ double sp_time_sub_t(sp_Time a, sp_Time b) {
859
+ return sp_time_ns_to_f(a.tv_sec - b.tv_sec, (int64_t)a.tv_nsec - b.tv_nsec);
860
+ }