prosebook 0.4.0 → 0.5.0
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.
- package/README.md +288 -28
- package/dist/bind-BIZNv8nU.js +62 -0
- package/dist/bind-BIZNv8nU.js.map +1 -0
- package/dist/bind-ZS86jHXk.d.ts +31 -0
- package/dist/bind-ZS86jHXk.d.ts.map +1 -0
- package/dist/bun.d.ts +11 -2
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js +77 -26
- package/dist/bun.js.map +1 -1
- package/dist/{bundle-CpUIWb9j.d.ts → bundle-CLHY_97D.d.ts} +2 -2
- package/dist/bundle-CLHY_97D.d.ts.map +1 -0
- package/dist/bundle-Ut6Y2X6N.js +217 -0
- package/dist/bundle-Ut6Y2X6N.js.map +1 -0
- package/dist/cli.js +55 -12
- package/dist/cli.js.map +1 -1
- package/dist/{config-nWZf9QmF.js → config-7T4hX_f7.js} +5 -3
- package/dist/{config-nWZf9QmF.js.map → config-7T4hX_f7.js.map} +1 -1
- package/dist/config-CzaJJQ60.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/{copy-B3V_QRkT.d.ts → copy-BXn0aYQn.d.ts} +37 -3
- package/dist/copy-BXn0aYQn.d.ts.map +1 -0
- package/dist/copy-Bv6S-Hr2.js +235 -0
- package/dist/copy-Bv6S-Hr2.js.map +1 -0
- package/dist/endpoints-Dhgowovz.js +217 -0
- package/dist/endpoints-Dhgowovz.js.map +1 -0
- package/dist/generate-1BeQAd1O.d.ts +72 -0
- package/dist/generate-1BeQAd1O.d.ts.map +1 -0
- package/dist/{generate-BW3N8EGb.js → generate-CKv86Iki.js} +1091 -309
- package/dist/generate-CKv86Iki.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/next-loader.d.ts +9 -2
- package/dist/next-loader.d.ts.map +1 -1
- package/dist/next-loader.js +12 -3
- package/dist/next-loader.js.map +1 -1
- package/dist/next.d.ts +10 -2
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +94 -21
- package/dist/next.js.map +1 -1
- package/dist/preact/index.d.ts +34 -0
- package/dist/preact/index.d.ts.map +1 -0
- package/dist/preact/index.js +72 -0
- package/dist/preact/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -14
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +9 -33
- package/dist/react/index.js.map +1 -1
- package/dist/react/server.d.ts +1 -1
- package/dist/react/server.js +1 -1
- package/dist/{sarif-B5nrMqrT.js → sarif-BnwYC78_.js} +8 -98
- package/dist/sarif-BnwYC78_.js.map +1 -0
- package/dist/{scan-Bx0_yDSi.js → scan-C0ap4Mpp.js} +145 -20
- package/dist/scan-C0ap4Mpp.js.map +1 -0
- package/dist/solid/index.d.ts +29 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +48 -0
- package/dist/solid/index.js.map +1 -0
- package/dist/svelte/Text.svelte +13 -0
- package/dist/svelte/Text.svelte.d.ts +20 -0
- package/dist/svelte/index.d.ts +31 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +35 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/tools.d.ts +43 -64
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -5
- package/dist/vite.d.ts +12 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +82 -25
- package/dist/vite.js.map +1 -1
- package/dist/vue/index.d.ts +43 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +76 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +63 -7
- package/src/bun.ts +77 -20
- package/src/cli.ts +62 -8
- package/src/index.ts +1 -1
- package/src/next-loader.ts +17 -5
- package/src/next.ts +120 -16
- package/src/preact/index.ts +91 -0
- package/src/react/index.tsx +12 -42
- package/src/runtime/bind.ts +96 -0
- package/src/runtime/copy.ts +148 -24
- package/src/solid/index.ts +67 -0
- package/src/svelte/Text.svelte +13 -0
- package/src/svelte/Text.svelte.d.ts +20 -0
- package/src/svelte/index.ts +65 -0
- package/src/tools/bundle.ts +143 -2
- package/src/tools/catalog.ts +10 -1
- package/src/tools/chunks.ts +142 -0
- package/src/tools/config.ts +1 -1
- package/src/tools/entry.ts +19 -20
- package/src/tools/exchange.ts +275 -0
- package/src/tools/extract.ts +2 -2
- package/src/tools/generate.ts +7 -1
- package/src/tools/index.ts +2 -1
- package/src/tools/native/android.ts +10 -25
- package/src/tools/native/apple.ts +70 -8
- package/src/tools/native/date.ts +219 -0
- package/src/tools/native/kotlin.ts +47 -6
- package/src/tools/native/model.ts +42 -16
- package/src/tools/native/printf.ts +46 -4
- package/src/tools/native/rust.ts +86 -5
- package/src/tools/native/swift.ts +62 -6
- package/src/tools/scan.ts +146 -25
- package/src/tools/todo.ts +6 -1
- package/src/vite.ts +121 -28
- package/src/vue/index.ts +117 -0
- package/dist/bundle-CpUIWb9j.d.ts.map +0 -1
- package/dist/bundle-DboItAre.js +0 -111
- package/dist/bundle-DboItAre.js.map +0 -1
- package/dist/compile-ClK9htKr.d.ts +0 -16
- package/dist/compile-ClK9htKr.d.ts.map +0 -1
- package/dist/copy-B3V_QRkT.d.ts.map +0 -1
- package/dist/copy-Bo_FIoLy.js +0 -154
- package/dist/copy-Bo_FIoLy.js.map +0 -1
- package/dist/endpoints-BgG_5Vpm.js +0 -96
- package/dist/endpoints-BgG_5Vpm.js.map +0 -1
- package/dist/generate-BW3N8EGb.js.map +0 -1
- package/dist/sarif-B5nrMqrT.js.map +0 -1
- package/dist/scan-Bx0_yDSi.js.map +0 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A date format taken apart at build time, as `number.ts` takes numbers apart: the text between the
|
|
3
|
+
* fields, how each field is written (a number, padded or not, or a name from a table), and the
|
|
4
|
+
* locale's digits. Read from `Intl.DateTimeFormat` and checked against it on dates that cover every
|
|
5
|
+
* month, weekday and hour before any code is written, so native code says what the browser says, or
|
|
6
|
+
* the build says why it can't.
|
|
7
|
+
*
|
|
8
|
+
* Native code reads the fields of the date as the clock shows them in its time zone, and only the
|
|
9
|
+
* Gregorian calendar: a format that names a time zone, or counts in another calendar, is refused.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A part of a formatted date. */
|
|
13
|
+
export type DatePiece =
|
|
14
|
+
| { type: 'text'; value: string }
|
|
15
|
+
/** The field as a number, padded with zeros to `pad` digits. `year2` is the year's last two digits. */
|
|
16
|
+
| { type: 'number'; field: 'year' | 'year2' | 'month' | 'day' | 'minute' | 'second'; pad: number }
|
|
17
|
+
/** The hour on the locale's clock: 1 to 12 (`h12`), 0 to 11 (`h11`), 0 to 23 (`h23`) or 1 to 24 (`h24`). */
|
|
18
|
+
| { type: 'hour'; cycle: HourCycle; pad: number }
|
|
19
|
+
/**
|
|
20
|
+
* The field as a name from `names`: a month's January first, a weekday's Sunday first, and the day
|
|
21
|
+
* period's before noon, then after, or one for each hour of the day.
|
|
22
|
+
*/
|
|
23
|
+
| { type: 'name'; field: 'month' | 'weekday' | 'dayPeriod'; names: string[] };
|
|
24
|
+
|
|
25
|
+
export type HourCycle = 'h11' | 'h12' | 'h23' | 'h24';
|
|
26
|
+
|
|
27
|
+
export interface DateSpec {
|
|
28
|
+
pieces: DatePiece[];
|
|
29
|
+
/** The numbering system's digits, 0 to 9, where it isn't ASCII's. */
|
|
30
|
+
digits?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A date as the clock shows it. */
|
|
34
|
+
export interface DateFields {
|
|
35
|
+
year: number;
|
|
36
|
+
/** 1 to 12. */
|
|
37
|
+
month: number;
|
|
38
|
+
day: number;
|
|
39
|
+
hour: number;
|
|
40
|
+
minute: number;
|
|
41
|
+
second: number;
|
|
42
|
+
/** 0 is Sunday. */
|
|
43
|
+
weekday: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Dates the spec must format as `Intl` does: every month and weekday, midnight, noon and the hours around them, and two-digit years. */
|
|
47
|
+
const SAMPLES: number[] = [
|
|
48
|
+
Date.UTC(2001, 0, 5, 1, 2, 3),
|
|
49
|
+
Date.UTC(2001, 10, 25, 13, 42, 53),
|
|
50
|
+
Date.UTC(1999, 11, 31, 23, 59, 59),
|
|
51
|
+
Date.UTC(2000, 1, 29, 0, 0, 0),
|
|
52
|
+
Date.UTC(2024, 6, 4, 12, 0, 0),
|
|
53
|
+
Date.UTC(2024, 6, 4, 12, 30, 5),
|
|
54
|
+
Date.UTC(1970, 0, 1, 0, 0, 0),
|
|
55
|
+
Date.UTC(2099, 8, 9, 9, 9, 9),
|
|
56
|
+
Date.UTC(2010, 4, 15, 18, 45, 0),
|
|
57
|
+
Date.UTC(2026, 2, 10, 6, 15, 30),
|
|
58
|
+
Date.UTC(2005, 9, 31, 21, 7, 41),
|
|
59
|
+
...Array.from({ length: 12 }, (_, m) => Date.UTC(2023, m, 1 + m * 2, m * 2, m * 5, m * 4)),
|
|
60
|
+
...Array.from({ length: 24 }, (_, h) => Date.UTC(2022, 3, 3 + (h % 7), h, 30, 0)),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** The fields of a date in UTC, which the spec is checked in. */
|
|
64
|
+
export function fieldsOf(date: number): DateFields {
|
|
65
|
+
const d = new Date(date);
|
|
66
|
+
return {
|
|
67
|
+
year: d.getUTCFullYear(),
|
|
68
|
+
month: d.getUTCMonth() + 1,
|
|
69
|
+
day: d.getUTCDate(),
|
|
70
|
+
hour: d.getUTCHours(),
|
|
71
|
+
minute: d.getUTCMinutes(),
|
|
72
|
+
second: d.getUTCSeconds(),
|
|
73
|
+
weekday: d.getUTCDay(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const HOUR_CYCLES = new Set<string>(['h11', 'h12', 'h23', 'h24']);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What a format with `options` takes in `locale`. Throws a sentence saying what a native target
|
|
81
|
+
* can't reproduce, which the caller puts in front of a fix.
|
|
82
|
+
*/
|
|
83
|
+
export function dateSpec(locale: string, options: Intl.DateTimeFormatOptions = {}): DateSpec {
|
|
84
|
+
if (options.timeZone !== undefined) throw new Error(`it names the time zone ${options.timeZone}, which native code can't look up`);
|
|
85
|
+
const format = new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' });
|
|
86
|
+
const resolved = format.resolvedOptions();
|
|
87
|
+
if (resolved.calendar !== 'gregory' && resolved.calendar !== 'iso8601') throw new Error(`it counts in the ${resolved.calendar} calendar`);
|
|
88
|
+
|
|
89
|
+
const native = new Intl.NumberFormat(locale, { numberingSystem: resolved.numberingSystem, useGrouping: false });
|
|
90
|
+
const digits = Array.from({ length: 10 }, (_, d) => native.format(d));
|
|
91
|
+
const ascii = (text: string) => text.replace(/./gu, (c) => (digits.includes(c) ? String(digits.indexOf(c)) : c));
|
|
92
|
+
/** The text `type` takes at `date`, in ASCII digits. */
|
|
93
|
+
const partAt = (date: number, type: string) => ascii(format.formatToParts(date).find((p) => p.type === type)?.value ?? '');
|
|
94
|
+
const isNumber = (text: string) => /^[0-9]+$/.test(text);
|
|
95
|
+
const padOf = (date: number, type: string) => (partAt(date, type).startsWith('0') ? 2 : 1);
|
|
96
|
+
|
|
97
|
+
const pieces: DatePiece[] = [];
|
|
98
|
+
for (const part of format.formatToParts(SAMPLES[1])) {
|
|
99
|
+
switch (part.type) {
|
|
100
|
+
case 'literal':
|
|
101
|
+
pieces.push({ type: 'text', value: part.value });
|
|
102
|
+
break;
|
|
103
|
+
case 'year':
|
|
104
|
+
pieces.push(ascii(part.value) === '01' ? { type: 'number', field: 'year2', pad: 2 } : { type: 'number', field: 'year', pad: 1 });
|
|
105
|
+
break;
|
|
106
|
+
case 'month': {
|
|
107
|
+
const names = Array.from(
|
|
108
|
+
{ length: 12 },
|
|
109
|
+
(_, m) => format.formatToParts(Date.UTC(2023, m, 15, 12)).find((p) => p.type === 'month')!.value,
|
|
110
|
+
);
|
|
111
|
+
if (names.every((name) => isNumber(ascii(name))))
|
|
112
|
+
pieces.push({ type: 'number', field: 'month', pad: padOf(Date.UTC(2023, 0, 15, 12), 'month') });
|
|
113
|
+
else pieces.push({ type: 'name', field: 'month', names });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'day':
|
|
117
|
+
pieces.push({ type: 'number', field: 'day', pad: padOf(Date.UTC(2023, 0, 5, 12), 'day') });
|
|
118
|
+
break;
|
|
119
|
+
case 'weekday':
|
|
120
|
+
// 2023-01-01 is a Sunday.
|
|
121
|
+
pieces.push({
|
|
122
|
+
type: 'name',
|
|
123
|
+
field: 'weekday',
|
|
124
|
+
names: Array.from(
|
|
125
|
+
{ length: 7 },
|
|
126
|
+
(_, d) => format.formatToParts(Date.UTC(2023, 0, 1 + d, 12)).find((p) => p.type === 'weekday')!.value,
|
|
127
|
+
),
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
case 'hour': {
|
|
131
|
+
const cycle = resolved.hourCycle;
|
|
132
|
+
if (cycle === undefined || !HOUR_CYCLES.has(cycle)) throw new Error('its hours follow no clock native code knows');
|
|
133
|
+
pieces.push({ type: 'hour', cycle, pad: padOf(Date.UTC(2023, 0, 5, 1), 'hour') });
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'minute':
|
|
137
|
+
case 'second':
|
|
138
|
+
pieces.push({ type: 'number', field: part.type, pad: padOf(Date.UTC(2023, 0, 5, 1, 5, 5), part.type) });
|
|
139
|
+
break;
|
|
140
|
+
case 'dayPeriod': {
|
|
141
|
+
const byHour = Array.from(
|
|
142
|
+
{ length: 24 },
|
|
143
|
+
(_, hour) => format.formatToParts(Date.UTC(2023, 0, 5, hour, 30)).find((p) => p.type === 'dayPeriod')!.value,
|
|
144
|
+
);
|
|
145
|
+
// Before noon and after, or a period by the hour (`zh-TW`'s 晚上, English's "at night").
|
|
146
|
+
const halves = byHour.every((name, hour) => name === byHour[hour < 12 ? 0 : 12]);
|
|
147
|
+
pieces.push({ type: 'name', field: 'dayPeriod', names: halves ? [byHour[0]!, byHour[12]!] : byHour });
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'era':
|
|
151
|
+
case 'fractionalSecond':
|
|
152
|
+
case 'timeZoneName':
|
|
153
|
+
case 'unknown':
|
|
154
|
+
// A calendar's own parts, such as `relatedYear`, which the calendar check has already refused.
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`it writes the ${part.type as string}, which native code can't`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const spec: DateSpec = { pieces, ...(digits.join('') !== '0123456789' && { digits }) };
|
|
160
|
+
for (const sample of SAMPLES) {
|
|
161
|
+
const expected = format.format(sample);
|
|
162
|
+
const actual = formatDate(spec, fieldsOf(sample));
|
|
163
|
+
if (actual !== expected)
|
|
164
|
+
throw new Error(`it writes ${new Date(sample).toISOString()} as "${expected}", which native code would write as "${actual}"`);
|
|
165
|
+
}
|
|
166
|
+
return spec;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The hour as a clock with `cycle` shows it. */
|
|
170
|
+
export function hourOn(cycle: HourCycle, hour: number): number {
|
|
171
|
+
if (cycle === 'h23') return hour;
|
|
172
|
+
if (cycle === 'h24') return hour === 0 ? 24 : hour;
|
|
173
|
+
if (cycle === 'h11') return hour % 12;
|
|
174
|
+
return ((hour + 11) % 12) + 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A date field as a target reads it, an integer expression. */
|
|
178
|
+
export type FieldAccess = (field: keyof DateFields) => string;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A piece's number, or a name piece's index into its names, as an expression in the integer
|
|
182
|
+
* arithmetic Rust, Swift and Kotlin share. Keep in step with `formatDate`.
|
|
183
|
+
*/
|
|
184
|
+
export function pieceExpr(piece: Exclude<DatePiece, { type: 'text' }>, at: FieldAccess): string {
|
|
185
|
+
if (piece.type === 'hour') {
|
|
186
|
+
const hour = at('hour');
|
|
187
|
+
if (piece.cycle === 'h23') return hour;
|
|
188
|
+
if (piece.cycle === 'h24') return `(${hour} + 23) % 24 + 1`;
|
|
189
|
+
return piece.cycle === 'h11' ? `${hour} % 12` : `(${hour} + 11) % 12 + 1`;
|
|
190
|
+
}
|
|
191
|
+
if (piece.type === 'number') return piece.field === 'year2' ? `(${at('year')} % 100 + 100) % 100` : at(piece.field);
|
|
192
|
+
if (piece.field === 'month') return `${at('month')} - 1`;
|
|
193
|
+
if (piece.field === 'weekday') return at('weekday');
|
|
194
|
+
return piece.names.length === 24 ? at('hour') : `${at('hour')} / 12`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Which of a name piece's `count` names `date` takes. */
|
|
198
|
+
function nameIndex(field: 'month' | 'weekday' | 'dayPeriod', count: number, date: DateFields): number {
|
|
199
|
+
if (field === 'month') return date.month - 1;
|
|
200
|
+
if (field === 'weekday') return date.weekday;
|
|
201
|
+
return count === 24 ? date.hour : date.hour < 12 ? 0 : 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** `date` as the spec writes it: the reference for the code each target generates. */
|
|
205
|
+
export function formatDate(spec: DateSpec, date: DateFields): string {
|
|
206
|
+
const digits = spec.digits;
|
|
207
|
+
const number = (value: number, pad: number) => {
|
|
208
|
+
const text = String(value).padStart(pad, '0');
|
|
209
|
+
return digits === undefined ? text : text.replace(/[0-9]/g, (d) => digits[Number(d)]!);
|
|
210
|
+
};
|
|
211
|
+
return spec.pieces
|
|
212
|
+
.map((piece) => {
|
|
213
|
+
if (piece.type === 'text') return piece.value;
|
|
214
|
+
if (piece.type === 'hour') return number(hourOn(piece.cycle, date.hour), piece.pad);
|
|
215
|
+
if (piece.type === 'number') return number(piece.field === 'year2' ? ((date.year % 100) + 100) % 100 : date[piece.field], piece.pad);
|
|
216
|
+
return piece.names[nameIndex(piece.field, piece.names.length, date)]!;
|
|
217
|
+
})
|
|
218
|
+
.join('');
|
|
219
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* a file-private object holding what formats them. For the JVM and Android.
|
|
4
4
|
*/
|
|
5
5
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
6
|
+
import { pieceExpr, type DateSpec } from './date.ts';
|
|
6
7
|
import { camel, checkNames, conditionCode, float, leading, stringLiteral, upper, wordsOf } from './emit.ts';
|
|
7
8
|
import type { NativeMessage, NativeModel, Part } from './model.ts';
|
|
8
9
|
import type { NumberSpec } from './number.ts';
|
|
@@ -36,6 +37,7 @@ function value(part: Exclude<Part, { type: 'text' }>, indent: string, rt: string
|
|
|
36
37
|
const of = part.offset === 0 ? argName(part.arg) : `${argName(part.arg)} - ${float(part.offset)}`;
|
|
37
38
|
return `${rt}.number(${of}, ${rt}.F${part.format}, ${lit(part.fallback)})`;
|
|
38
39
|
}
|
|
40
|
+
if (part.type === 'date') return `${rt}.d${part.format}(${argName(part.arg)})`;
|
|
39
41
|
if (part.type === 'select') {
|
|
40
42
|
const arms = part.arms.map((a) => `${inner}${lit(a.key)} -> ${expr(a.parts, inner, rt)}\n`).join('');
|
|
41
43
|
return `when (${argName(part.arg)}) {\n${arms}${inner}else -> ${expr(part.other, inner, rt)}\n${indent}}`;
|
|
@@ -59,10 +61,17 @@ function member(m: NativeMessage, name: string, rt: string): string {
|
|
|
59
61
|
const head = doc(argDocs.length > 0 ? [...m.doc, '', ...argDocs] : m.doc, ' ');
|
|
60
62
|
const body = `when (this) {\n${branches} }`;
|
|
61
63
|
if (m.args.length === 0) return `${head} val ${name}: String\n get() = ${body}\n`;
|
|
62
|
-
const params = m.args
|
|
64
|
+
const params = m.args
|
|
65
|
+
.map((a) => `${argName(a.name)}: ${a.type === 'number' ? 'Number' : a.type === 'date' ? 'java.time.Instant' : 'String'}`)
|
|
66
|
+
.join(', ');
|
|
63
67
|
const numbers = m.args
|
|
64
|
-
.
|
|
65
|
-
|
|
68
|
+
.map((a) =>
|
|
69
|
+
a.type === 'number'
|
|
70
|
+
? ` val ${argName(a.name)} = ${argName(a.name)}.toDouble()\n`
|
|
71
|
+
: a.type === 'date'
|
|
72
|
+
? ` val ${argName(a.name)} = ${argName(a.name)}.atZone(java.time.ZoneId.systemDefault())\n`
|
|
73
|
+
: '',
|
|
74
|
+
)
|
|
66
75
|
.join('');
|
|
67
76
|
return `${head} fun ${name}(${params}): String {\n${numbers} return ${body}\n }\n`;
|
|
68
77
|
}
|
|
@@ -90,6 +99,38 @@ function formatConst(spec: NumberSpec, i: number): string {
|
|
|
90
99
|
return ` val F${i} = Format(\n${fields.map((f) => ` ${f}`).join(',\n')},\n )\n`;
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
const FIELDS: Record<string, string> = {
|
|
103
|
+
year: 't.year',
|
|
104
|
+
month: 't.monthValue',
|
|
105
|
+
day: 't.dayOfMonth',
|
|
106
|
+
hour: 't.hour',
|
|
107
|
+
minute: 't.minute',
|
|
108
|
+
second: 't.second',
|
|
109
|
+
weekday: '(t.dayOfWeek.value % 7)',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** A date format as a function of the date in the system's time zone. */
|
|
113
|
+
function dateFn(spec: DateSpec, i: number): string {
|
|
114
|
+
const digits = spec.digits === undefined ? 'null' : `G${i}`;
|
|
115
|
+
const at = (field: string) => FIELDS[field]!;
|
|
116
|
+
const lines = spec.pieces.map((piece) => {
|
|
117
|
+
if (piece.type === 'text') return `append(${lit(piece.value)})`;
|
|
118
|
+
if (piece.type === 'name') return `append(arrayOf(${piece.names.map(lit).join(', ')})[${pieceExpr(piece, at)}])`;
|
|
119
|
+
return `append(pad(${pieceExpr(piece, at)}, ${piece.pad}, ${digits}))`;
|
|
120
|
+
});
|
|
121
|
+
const table = spec.digits === undefined ? '' : ` private val G${i} = arrayOf(${spec.digits.map(lit).join(', ')})\n`;
|
|
122
|
+
const body = lines.map((l) => ` ${l}\n`).join('');
|
|
123
|
+
return `${table} fun d${i}(t: java.time.ZonedDateTime): String = buildString {\n${body} }\n`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const DATE = `
|
|
127
|
+
/** \`value\` written with at least \`width\` digits, in \`digits\` where the locale has its own. */
|
|
128
|
+
private fun pad(value: Int, width: Int, digits: Array<String>?): String {
|
|
129
|
+
val text = value.toString().padStart(width, '0')
|
|
130
|
+
return if (digits == null) text else text.map { if (it in '0'..'9') digits[it - '0'] else it.toString() }.joinToString("")
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
|
|
93
134
|
const NUMBER = `
|
|
94
135
|
/** A number format, taken apart when the file was generated. */
|
|
95
136
|
class Format(
|
|
@@ -225,12 +266,12 @@ export function kotlinFile(model: NativeModel, target: NativeTarget & { name: st
|
|
|
225
266
|
})
|
|
226
267
|
.join('\n');
|
|
227
268
|
const runtime =
|
|
228
|
-
model.formats.length + model.rules.length === 0
|
|
269
|
+
model.formats.length + model.rules.length + model.dates.length === 0
|
|
229
270
|
? ''
|
|
230
271
|
: `
|
|
231
272
|
/** What formats the messages, generated with them. */
|
|
232
|
-
private object ${rt} {${NUMBER}${model.rules.length > 0 ? PLURAL : ''}
|
|
233
|
-
${model.formats.map(formatConst).join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
273
|
+
private object ${rt} {${model.formats.length + model.rules.length > 0 ? NUMBER : ''}${model.rules.length > 0 ? PLURAL : ''}${model.dates.length > 0 ? DATE : ''}
|
|
274
|
+
${[...model.formats.map(formatConst), ...model.dates.map(dateFn)].join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
234
275
|
`;
|
|
235
276
|
|
|
236
277
|
return `${banner}
|
|
@@ -9,7 +9,8 @@ import { chainOf } from '#src/runtime/locale.ts';
|
|
|
9
9
|
import type { Catalog, Message, Problem } from '#src/tools/catalog.ts';
|
|
10
10
|
import type { Entry } from '#src/tools/compile.ts';
|
|
11
11
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
12
|
-
import { numberOptions, optionArgsOf, placeholderOf, selectionOf, TYPE, type MessageFormatElement } from '#src/tools/icu.ts';
|
|
12
|
+
import { dateOptions, numberOptions, optionArgsOf, placeholderOf, selectionOf, TYPE, type MessageFormatElement } from '#src/tools/icu.ts';
|
|
13
|
+
import { dateSpec, type DateSpec } from './date.ts';
|
|
13
14
|
import { docText } from './emit.ts';
|
|
14
15
|
import { numberSpec, roundingOf, type NumberSpec, type Rounding } from './number.ts';
|
|
15
16
|
import { pluralRules, type PluralRules } from './plurals.ts';
|
|
@@ -19,6 +20,8 @@ export type Part =
|
|
|
19
20
|
| { type: 'string'; arg: string }
|
|
20
21
|
/** The argument less `offset`, in format `format`; `fallback` where it isn't a number. */
|
|
21
22
|
| { type: 'number'; arg: string; offset: number; format: number; fallback: string }
|
|
23
|
+
/** The argument, a date, in date format `format`. */
|
|
24
|
+
| { type: 'date'; arg: string; format: number }
|
|
22
25
|
| { type: 'select'; arg: string; arms: Arm[]; other: Part[] }
|
|
23
26
|
| {
|
|
24
27
|
type: 'plural';
|
|
@@ -45,7 +48,7 @@ export interface NativeMessage {
|
|
|
45
48
|
words: string[];
|
|
46
49
|
/** The source text, its context, and what's asked of it. */
|
|
47
50
|
doc: string[];
|
|
48
|
-
args: { name: string; type: 'string' | 'number'; doc?: string }[];
|
|
51
|
+
args: { name: string; type: 'string' | 'number' | 'date'; doc?: string }[];
|
|
49
52
|
/** A tree per locale that renders it its own way; every locale is in one. */
|
|
50
53
|
bodies: { locales: string[]; parts: Part[] }[];
|
|
51
54
|
}
|
|
@@ -56,6 +59,7 @@ export interface NativeModel {
|
|
|
56
59
|
source: string;
|
|
57
60
|
messages: NativeMessage[];
|
|
58
61
|
formats: NumberSpec[];
|
|
62
|
+
dates: DateSpec[];
|
|
59
63
|
rules: { name: string; rules: PluralRules }[];
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -106,6 +110,7 @@ export function nativeModel(
|
|
|
106
110
|
const source = locales[0]!;
|
|
107
111
|
const asts = new Map([...entries].map(([locale, list]) => [locale, new Map(list)]));
|
|
108
112
|
const formats = new Numbered<NumberSpec>();
|
|
113
|
+
const dates = new Numbered<DateSpec>();
|
|
109
114
|
const rules = new Numbered<{ name: string; rules: PluralRules }>();
|
|
110
115
|
const problems: Problem[] = [];
|
|
111
116
|
const messages: NativeMessage[] = [];
|
|
@@ -124,7 +129,7 @@ export function nativeModel(
|
|
|
124
129
|
continue;
|
|
125
130
|
}
|
|
126
131
|
try {
|
|
127
|
-
bodies.set(found, { locales: [locale], parts: parts(asts.get(found)!.get(m.key)!, found, formats, rules, null) });
|
|
132
|
+
bodies.set(found, { locales: [locale], parts: parts(asts.get(found)!.get(m.key)!, found, { formats, dates, rules }, null) });
|
|
128
133
|
} catch (e) {
|
|
129
134
|
failed = true;
|
|
130
135
|
problems.push({
|
|
@@ -142,13 +147,13 @@ export function nativeModel(
|
|
|
142
147
|
doc: docOf(m),
|
|
143
148
|
args: [...m.args].map(([name, type]) => ({
|
|
144
149
|
name,
|
|
145
|
-
type: type === 'number' ? 'number' : 'string',
|
|
150
|
+
type: type === 'number' ? 'number' : type === 'Date | number' ? 'date' : 'string',
|
|
146
151
|
doc: m.argDocs.has(name) ? docText(m.argDocs.get(name)!) : undefined,
|
|
147
152
|
})),
|
|
148
153
|
bodies: [...bodies.values()],
|
|
149
154
|
});
|
|
150
155
|
}
|
|
151
|
-
return { model: { locales, source, messages, formats: formats.list, rules: rules.list }, problems };
|
|
156
|
+
return { model: { locales, source, messages, formats: formats.list, dates: dates.list, rules: rules.list }, problems };
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
export function fileOf(m: Message, locale: string, source: string): string {
|
|
@@ -168,6 +173,33 @@ export const LEAVE_OUT = "or leave the key out of this target's `keys`";
|
|
|
168
173
|
/** Specs by locale and options, kept across runs: taking a format apart checks it against `Intl` dozens of times. */
|
|
169
174
|
const SPECS = new Map<string, NumberSpec | Error>();
|
|
170
175
|
|
|
176
|
+
/** What a file declares, numbered as the messages use it. */
|
|
177
|
+
interface Declared {
|
|
178
|
+
formats: Numbered<NumberSpec>;
|
|
179
|
+
dates: Numbered<DateSpec>;
|
|
180
|
+
rules: Numbered<{ name: string; rules: PluralRules }>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Date specs as `SPECS` keeps number specs. */
|
|
184
|
+
const DATE_SPECS = new Map<string, DateSpec | Error>();
|
|
185
|
+
|
|
186
|
+
function date(dates: Numbered<DateSpec>, locale: string, options: Intl.DateTimeFormatOptions | undefined): number {
|
|
187
|
+
const key = JSON.stringify([locale, options ?? {}]);
|
|
188
|
+
let spec = DATE_SPECS.get(key);
|
|
189
|
+
if (spec === undefined) {
|
|
190
|
+
try {
|
|
191
|
+
spec = dateSpec(locale, options);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
spec = new Error(`native code can't format this date in ${locale}: ${(e as Error).message}. Format it another way, ${LEAVE_OUT}`, {
|
|
194
|
+
cause: e,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
DATE_SPECS.set(key, spec);
|
|
198
|
+
}
|
|
199
|
+
if (spec instanceof Error) throw spec;
|
|
200
|
+
return dates.add(spec);
|
|
201
|
+
}
|
|
202
|
+
|
|
171
203
|
function format(formats: Numbered<NumberSpec>, locale: string, options: Intl.NumberFormatOptions | undefined): number {
|
|
172
204
|
const key = JSON.stringify([locale, options ?? {}]);
|
|
173
205
|
let spec = SPECS.get(key);
|
|
@@ -187,13 +219,8 @@ function format(formats: Numbered<NumberSpec>, locale: string, options: Intl.Num
|
|
|
187
219
|
|
|
188
220
|
type Pound = { arg: string; offset: number } | null;
|
|
189
221
|
|
|
190
|
-
function parts(
|
|
191
|
-
|
|
192
|
-
locale: string,
|
|
193
|
-
formats: Numbered<NumberSpec>,
|
|
194
|
-
rules: Numbered<{ name: string; rules: PluralRules }>,
|
|
195
|
-
pound: Pound,
|
|
196
|
-
): Part[] {
|
|
222
|
+
function parts(elements: MessageFormatElement[], locale: string, declared: Declared, pound: Pound): Part[] {
|
|
223
|
+
const { formats, dates, rules } = declared;
|
|
197
224
|
const out: Part[] = [];
|
|
198
225
|
const text = (value: string) => {
|
|
199
226
|
const last = out.at(-1);
|
|
@@ -207,7 +234,7 @@ function parts(
|
|
|
207
234
|
if (isolate !== undefined) text(isolate[1]);
|
|
208
235
|
};
|
|
209
236
|
const arms = (options: Record<string, { value: MessageFormatElement[] }>, own: Pound) =>
|
|
210
|
-
Object.entries(options).map(([key, option]) => ({ key, parts: parts(option.value, locale,
|
|
237
|
+
Object.entries(options).map(([key, option]) => ({ key, parts: parts(option.value, locale, declared, own) }));
|
|
211
238
|
|
|
212
239
|
for (const el of elements) {
|
|
213
240
|
if (Object.keys(optionArgsOf(el) ?? {}).length > 0) {
|
|
@@ -216,9 +243,8 @@ function parts(
|
|
|
216
243
|
);
|
|
217
244
|
}
|
|
218
245
|
if (isDateElement(el) || isTimeElement(el)) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
);
|
|
246
|
+
placed(el, el.value, { type: 'date', arg: el.value, format: date(dates, locale, dateOptions(el)) });
|
|
247
|
+
continue;
|
|
222
248
|
}
|
|
223
249
|
if (isNumberElement(el)) {
|
|
224
250
|
const fallback = placeholderOf(el, el.value).fallback;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Messages as printf-style format strings, for the targets whose platform fills values in itself:
|
|
3
|
-
* Android's resources and Apple's `.strings` files. Text and plain `{name}` placeholders, as `%N$s`
|
|
3
|
+
* Android's resources and Apple's `.strings` and `.stringsdict` files. Text and plain `{name}` placeholders, as `%N$s`
|
|
4
4
|
* with N the argument's place in the message's arguments, so a translation can put them in another
|
|
5
5
|
* order; anything `Intl` would format fails with the fix.
|
|
6
6
|
*/
|
|
7
7
|
import { isDateElement, isNumberElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
|
|
8
8
|
|
|
9
|
+
import type { PluralElement } from '@formatjs/icu-messageformat-parser';
|
|
9
10
|
import type { Message } from '#src/tools/catalog.ts';
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
import { numberOptions, placeholderOf, selectionOf, TYPE, type MessageFormatElement } from '#src/tools/icu.ts';
|
|
11
13
|
import { docText } from './emit.ts';
|
|
12
14
|
import { LEAVE_OUT } from './model.ts';
|
|
13
15
|
|
|
@@ -19,6 +21,8 @@ export interface Printf {
|
|
|
19
21
|
holder: string;
|
|
20
22
|
/** A string argument's conversion: `s`, or `@` for Apple's objects. */
|
|
21
23
|
string: string;
|
|
24
|
+
/** A count's conversion: `d`, or `lld` for Apple's 64-bit `Int`. */
|
|
25
|
+
count: string;
|
|
22
26
|
/** Text as the file holds it; `formatted` when the string has arguments, so `%` must be doubled. */
|
|
23
27
|
escape: (text: string, formatted: boolean) => string;
|
|
24
28
|
/** The problem with a plural the target can't hold where it is. */
|
|
@@ -42,7 +46,45 @@ function isCount(el: MessageFormatElement, count: string): boolean {
|
|
|
42
46
|
return options.length === 0 || (options.length === 1 && options[0]![0] === 'maximumFractionDigits' && options[0]![1] === 0);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
/**
|
|
49
|
+
/** What a platform's plurals are called in problems, and what they can't hold. */
|
|
50
|
+
export interface PluralHolder {
|
|
51
|
+
/** `Android`, as in "Android selects on the count as an integer". */
|
|
52
|
+
platform: string;
|
|
53
|
+
/** `Android's <plurals>`. */
|
|
54
|
+
plurals: string;
|
|
55
|
+
/** Why an `=N` arm has no place, after "have none for one number". */
|
|
56
|
+
exact: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Throws, with the fix, where a plural asks for more than a platform's plurals pick on: one count,
|
|
61
|
+
* as an integer, by its cardinal category.
|
|
62
|
+
*/
|
|
63
|
+
export function checkPlural(plural: PluralElement, holder: PluralHolder): void {
|
|
64
|
+
const named = `{${plural.value}}`;
|
|
65
|
+
if (plural.pluralType === 'ordinal') {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${named} is an ordinal, and ${holder.plurals} are cardinal only: write a key per case and pick one in code, ${LEAVE_OUT}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (plural.offset !== 0)
|
|
71
|
+
throw new Error(`${named} has an offset, which ${holder.plurals} don't: count without it, and say the rest in each arm, ${LEAVE_OUT}`);
|
|
72
|
+
|
|
73
|
+
const selection = selectionOf(plural);
|
|
74
|
+
if (selection !== undefined && (selection.fn.name !== 'integer' || Object.keys(selection.fn.options).length > 0)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`${named} selects with :${selection.fn.name} and its options, and ${holder.platform} selects on the count as an integer: select with a bare :number or :integer, ${LEAVE_OUT}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const exact = Object.keys(plural.options).filter((k) => k.startsWith('='));
|
|
80
|
+
if (exact.length > 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${named} has the arm ${exact.join(', ')}, and ${holder.plurals} have none for one number${holder.exact}: say it in a category's arm, or write a key for it and pick that in code, ${LEAVE_OUT}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `elements` as a format string over `args`; inside a plural, its `count` is `%N$d` (or `lld`). */
|
|
46
88
|
export function formatString(elements: MessageFormatElement[], args: string[], printf: Printf, count?: string): string {
|
|
47
89
|
const formatted = args.length > 0;
|
|
48
90
|
const place = (el: MessageFormatElement, name: string, conversion: string) => {
|
|
@@ -55,7 +97,7 @@ export function formatString(elements: MessageFormatElement[], args: string[], p
|
|
|
55
97
|
let out = '';
|
|
56
98
|
for (const el of elements) {
|
|
57
99
|
if (count !== undefined && isCount(el, count)) {
|
|
58
|
-
out += place(el, count,
|
|
100
|
+
out += place(el, count, printf.count);
|
|
59
101
|
continue;
|
|
60
102
|
}
|
|
61
103
|
if (isDateElement(el) || isTimeElement(el)) {
|
package/src/tools/native/rust.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* a private `rt` module, so no argument a message names can shadow it.
|
|
4
4
|
*/
|
|
5
5
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
6
|
+
import { pieceExpr, type DateSpec } from './date.ts';
|
|
6
7
|
import { checkNames, conditionCode, float, leading, pascal, snake, stringLiteral, wordsOf } from './emit.ts';
|
|
7
8
|
import type { NativeMessage, NativeModel, Part } from './model.ts';
|
|
8
9
|
import type { NumberSpec } from './number.ts';
|
|
@@ -50,6 +51,7 @@ function value(part: Exclude<Part, { type: 'text' }>, indent: string): string {
|
|
|
50
51
|
const of = part.offset === 0 ? argName(part.arg) : `${argName(part.arg)} - ${float(part.offset)}`;
|
|
51
52
|
return `rt::number(${of}, &rt::F${part.format}, ${lit(part.fallback)})`;
|
|
52
53
|
}
|
|
54
|
+
if (part.type === 'date') return `rt::D${part.format}(&${argName(part.arg)})`;
|
|
53
55
|
if (part.type === 'select') {
|
|
54
56
|
const arms = part.arms.map((a) => `${inner}${lit(a.key)} => ${expr(a.parts, inner)},\n`).join('');
|
|
55
57
|
return `match ${argName(part.arg)} {\n${arms}${inner}_ => ${expr(part.other, inner)},\n${indent}}`;
|
|
@@ -74,7 +76,9 @@ function method(m: NativeMessage, name: string, enumName: string): string {
|
|
|
74
76
|
const isStatic = m.bodies.every((b) => b.parts.every((p) => p.type === 'text'));
|
|
75
77
|
if (isStatic && m.args.length === 0)
|
|
76
78
|
return `${head} pub fn ${name}(self) -> &'static str {\n match self {\n${arms((parts) => lit(parts[0]?.type === 'text' ? parts[0].value : ''))} }\n }\n`;
|
|
77
|
-
const params = m.args
|
|
79
|
+
const params = m.args
|
|
80
|
+
.map((a) => `, ${argName(a.name)}: ${a.type === 'number' ? 'impl Number' : a.type === 'date' ? 'DateTime' : '&str'}`)
|
|
81
|
+
.join('');
|
|
78
82
|
const argDocs = m.args.filter((a) => a.doc !== undefined).map((a) => `* \`${argName(a.name)}\`: ${a.doc}`);
|
|
79
83
|
const docs = argDocs.length > 0 ? `${head} ///\n${doc(argDocs, ' ')}` : head;
|
|
80
84
|
const numbers = m.args
|
|
@@ -107,6 +111,81 @@ function formatConst(spec: NumberSpec, i: number): string {
|
|
|
107
111
|
return ` pub const F${i}: Format = Format {\n${fields.map((f) => ` ${f},\n`).join('')} };\n`;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
/** A date format as a function of the date. */
|
|
115
|
+
function dateFn(spec: DateSpec, i: number): string {
|
|
116
|
+
const digits = spec.digits === undefined ? 'None' : `Some(G${i})`;
|
|
117
|
+
const at = (field: string) => (field === 'weekday' ? 't.weekday() as i64' : `t.${field} as i64`);
|
|
118
|
+
const lines = spec.pieces.map((piece) => {
|
|
119
|
+
if (piece.type === 'text') return `s += ${lit(piece.value)};`;
|
|
120
|
+
if (piece.type === 'name') return `s += [${piece.names.map(lit).join(', ')}][(${pieceExpr(piece, at)}) as usize];`;
|
|
121
|
+
return `s += &pad(${pieceExpr(piece, at)}, ${piece.pad}, ${digits});`;
|
|
122
|
+
});
|
|
123
|
+
const table = spec.digits === undefined ? '' : ` const G${i}: [&str; 10] = [${spec.digits.map(lit).join(', ')}];\n`;
|
|
124
|
+
const body = lines.map((l) => ` ${l}\n`).join('');
|
|
125
|
+
return `${table} pub fn D${i}(t: &super::DateTime) -> String {\n let mut s = String::new();\n${body} s\n }\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const DATE = `
|
|
129
|
+
/// \`value\` written with at least \`width\` digits, in \`digits\` where the locale has its own.
|
|
130
|
+
fn pad(value: i64, width: usize, digits: Option<[&'static str; 10]>) -> String {
|
|
131
|
+
let text = format!("{:0width$}", value, width = width);
|
|
132
|
+
match digits {
|
|
133
|
+
Some(digits) => text.bytes().map(|b| if b.is_ascii_digit() { digits[(b - b'0') as usize] } else { "-" }).collect(),
|
|
134
|
+
None => text,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
const DATE_TIME = `
|
|
140
|
+
/// A date and time as a clock shows it, in the time zone it is shown in.
|
|
141
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
142
|
+
pub struct DateTime {
|
|
143
|
+
pub year: i32,
|
|
144
|
+
/// 1 to 12.
|
|
145
|
+
pub month: u8,
|
|
146
|
+
pub day: u8,
|
|
147
|
+
/// 0 to 23.
|
|
148
|
+
pub hour: u8,
|
|
149
|
+
pub minute: u8,
|
|
150
|
+
pub second: u8,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
impl DateTime {
|
|
154
|
+
/// The time \`seconds\` after the Unix epoch, on a clock \`offset\` seconds ahead of UTC: the
|
|
155
|
+
/// local time's offset where it is shown, which Rust's standard library can't look up.
|
|
156
|
+
pub fn from_unix(seconds: i64, offset: i32) -> DateTime {
|
|
157
|
+
let local = seconds + offset as i64;
|
|
158
|
+
let (days, time) = (local.div_euclid(86_400), local.rem_euclid(86_400));
|
|
159
|
+
// Howard Hinnant's civil_from_days: the Gregorian date \`days\` after 1970-01-01.
|
|
160
|
+
let z = days + 719_468;
|
|
161
|
+
let (era, doe) = (z.div_euclid(146_097), z.rem_euclid(146_097));
|
|
162
|
+
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
|
163
|
+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
164
|
+
let mp = (5 * doy + 2) / 153;
|
|
165
|
+
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
166
|
+
DateTime {
|
|
167
|
+
year: (yoe + era * 400 + if month <= 2 { 1 } else { 0 }) as i32,
|
|
168
|
+
month: month as u8,
|
|
169
|
+
day: (doy - (153 * mp + 2) / 5 + 1) as u8,
|
|
170
|
+
hour: (time / 3600) as u8,
|
|
171
|
+
minute: (time / 60 % 60) as u8,
|
|
172
|
+
second: (time % 60) as u8,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// The day of the week, 0 for Sunday.
|
|
177
|
+
pub fn weekday(self) -> u8 {
|
|
178
|
+
// days_from_civil, its inverse.
|
|
179
|
+
let m = self.month as i64;
|
|
180
|
+
let y = self.year as i64 - if m <= 2 { 1 } else { 0 };
|
|
181
|
+
let (era, yoe) = (y.div_euclid(400), y.rem_euclid(400));
|
|
182
|
+
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + self.day as i64 - 1;
|
|
183
|
+
let days = era * 146_097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719_468;
|
|
184
|
+
(days + 4).rem_euclid(7) as u8
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
|
|
110
189
|
const NUMBER = `
|
|
111
190
|
/// A number format, taken apart when the file was generated.
|
|
112
191
|
pub struct Format {
|
|
@@ -300,6 +379,8 @@ export function rustModule(model: NativeModel, target: NativeTarget & { name: st
|
|
|
300
379
|
const tags = model.locales.map((l) => ` ${name}::${variant(l)} => ${lit(l)},\n`).join('');
|
|
301
380
|
const methods = model.messages.map((m, i) => method(m, names[i]![1], name)).join('\n');
|
|
302
381
|
const usesNumbers = model.messages.some((m) => m.args.some((a) => a.type === 'number'));
|
|
382
|
+
const usesDates = model.dates.length > 0;
|
|
383
|
+
const usesFormats = model.formats.length + model.rules.length > 0;
|
|
303
384
|
const rules = model.rules
|
|
304
385
|
.map(({ name: rule, rules }, i) => {
|
|
305
386
|
const checks = rules.map(
|
|
@@ -310,17 +391,17 @@ export function rustModule(model: NativeModel, target: NativeTarget & { name: st
|
|
|
310
391
|
})
|
|
311
392
|
.join('\n');
|
|
312
393
|
const runtime =
|
|
313
|
-
|
|
394
|
+
!usesFormats && !usesDates
|
|
314
395
|
? ''
|
|
315
396
|
: `
|
|
316
397
|
/// What formats the messages, generated with them.
|
|
317
|
-
mod rt {${NUMBER}${model.rules.length > 0 ? PLURAL : ''}
|
|
318
|
-
${model.formats.map(formatConst).join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
398
|
+
mod rt {${usesFormats ? NUMBER : ''}${model.rules.length > 0 ? PLURAL : ''}${usesDates ? DATE : ''}
|
|
399
|
+
${[...model.formats.map(formatConst), ...model.dates.map(dateFn)].join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
319
400
|
`;
|
|
320
401
|
|
|
321
402
|
return `${banner}
|
|
322
403
|
#![allow(dead_code, non_snake_case, clippy::all)]
|
|
323
|
-
${usesNumbers ? NUMBER_TRAIT : ''}
|
|
404
|
+
${usesNumbers ? NUMBER_TRAIT : ''}${usesDates ? DATE_TIME : ''}
|
|
324
405
|
/// A locale the catalog is written or translated in, and its messages.
|
|
325
406
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
326
407
|
pub enum ${name} {
|