@blamejs/core 0.5.13 → 0.5.15
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/CHANGELOG.md +2 -0
- package/index.js +4 -0
- package/lib/archive.js +241 -0
- package/lib/time.js +289 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
|
|
12
|
+
- **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
|
|
11
13
|
- **0.5.12** (2026-04-30) — b.middleware.requestLog: HTTP access-log middleware
|
|
12
14
|
- **0.5.11** (2026-04-30) — b.config: schema-validated environment configuration
|
|
13
15
|
- **0.5.10** (2026-04-30) — b.middleware.sse: Server-Sent Events
|
package/index.js
CHANGED
|
@@ -105,9 +105,11 @@ var staticServe = require("./lib/static");
|
|
|
105
105
|
var forms = require("./lib/forms");
|
|
106
106
|
var app = require("./lib/app");
|
|
107
107
|
var jobs = require("./lib/jobs");
|
|
108
|
+
var archive = require("./lib/archive");
|
|
108
109
|
var breakGlass = require("./lib/break-glass");
|
|
109
110
|
var config = require("./lib/config");
|
|
110
111
|
var csv = require("./lib/csv");
|
|
112
|
+
var time = require("./lib/time");
|
|
111
113
|
var uuid = require("./lib/uuid");
|
|
112
114
|
var mail = require("./lib/mail");
|
|
113
115
|
var mailBounce = require("./lib/mail-bounce");
|
|
@@ -210,9 +212,11 @@ module.exports = {
|
|
|
210
212
|
forms: forms,
|
|
211
213
|
createApp: app.createApp,
|
|
212
214
|
jobs: jobs,
|
|
215
|
+
archive: archive,
|
|
213
216
|
breakGlass: breakGlass,
|
|
214
217
|
config: config,
|
|
215
218
|
csv: csv,
|
|
219
|
+
time: time,
|
|
216
220
|
uuid: uuid,
|
|
217
221
|
mail: mail,
|
|
218
222
|
mailBounce: mailBounce,
|
package/lib/archive.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* archive — ZIP creation. Operator-data-export shape ("download my
|
|
4
|
+
* data as a zip"), log archives, plain-zip exports for users.
|
|
5
|
+
*
|
|
6
|
+
* var archive = b.archive.zip();
|
|
7
|
+
* archive.addFile("readme.txt", "Hello\n");
|
|
8
|
+
* archive.addFile("data/users.csv", csvBytes, { method: "deflate" });
|
|
9
|
+
* archive.addFile("avatars/me.png", pngBuf, { method: "store" }); // already-compressed
|
|
10
|
+
* var zipBytes = archive.toBuffer();
|
|
11
|
+
*
|
|
12
|
+
* // OR write directly to disk:
|
|
13
|
+
* archive.writeTo("/tmp/export.zip");
|
|
14
|
+
*
|
|
15
|
+
* Format support:
|
|
16
|
+
* - Stored (no compression — for already-compressed inputs like
|
|
17
|
+
* PNG / JPEG / mp4)
|
|
18
|
+
* - Deflate via node:zlib's deflateRawSync (default for everything else)
|
|
19
|
+
* - File names with / are honored — directory entries are implicit;
|
|
20
|
+
* extractors create the directory structure on demand
|
|
21
|
+
* - UTF-8 file names (sets the EFS bit per APPNOTE 6.3.4)
|
|
22
|
+
* - Modification time defaults to "now"; operators override per file
|
|
23
|
+
*
|
|
24
|
+
* v1 scope cuts (deferred):
|
|
25
|
+
* - ZIP64 (>4 GiB archives, >65535 files) — operators with that
|
|
26
|
+
* scale bring their own
|
|
27
|
+
* - Encryption — `b.crypto.encryptPacked` produces a sealed bundle
|
|
28
|
+
* for the operator's encryption-at-rest needs; ZIP-native
|
|
29
|
+
* password encryption is broken-by-design
|
|
30
|
+
* - Streaming write (toStream) — toBuffer() is enough for the
|
|
31
|
+
* "download my data" shape; operators streaming gigabytes
|
|
32
|
+
* have a different toolset
|
|
33
|
+
* - Reading / extraction — write-only for now
|
|
34
|
+
*/
|
|
35
|
+
var zlib = require("node:zlib");
|
|
36
|
+
var fs = require("node:fs");
|
|
37
|
+
var nodeCrypto = require("node:crypto");
|
|
38
|
+
var { defineClass } = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
var ArchiveError = defineClass("ArchiveError", { alwaysPermanent: true });
|
|
41
|
+
|
|
42
|
+
// ZIP signatures
|
|
43
|
+
var SIG_LFH = 0x04034b50; // local file header
|
|
44
|
+
var SIG_CFH = 0x02014b50; // central directory file header
|
|
45
|
+
var SIG_EOCD = 0x06054b50; // end of central directory
|
|
46
|
+
|
|
47
|
+
// Compression methods
|
|
48
|
+
var METHOD_STORE = 0;
|
|
49
|
+
var METHOD_DEFLATE = 8;
|
|
50
|
+
|
|
51
|
+
// CRC-32 — IEEE 802.3 polynomial. node:crypto has no native CRC32, so
|
|
52
|
+
// we vendor the standard table-driven implementation.
|
|
53
|
+
var CRC32_TABLE = (function () {
|
|
54
|
+
var t = new Uint32Array(256);
|
|
55
|
+
for (var i = 0; i < 256; i++) {
|
|
56
|
+
var c = i;
|
|
57
|
+
for (var j = 0; j < 8; j++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
58
|
+
t[i] = c >>> 0;
|
|
59
|
+
}
|
|
60
|
+
return t;
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
function _crc32(buf) {
|
|
64
|
+
var crc = 0xffffffff;
|
|
65
|
+
for (var i = 0; i < buf.length; i++) {
|
|
66
|
+
crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
|
67
|
+
}
|
|
68
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MS-DOS date/time encoding — APPNOTE 4.4.6
|
|
72
|
+
function _msdosDateTime(date) {
|
|
73
|
+
var d = date instanceof Date ? date : new Date(date);
|
|
74
|
+
if (isNaN(d.getTime())) d = new Date();
|
|
75
|
+
var dosTime = ((d.getHours() & 0x1f) << 11) |
|
|
76
|
+
((d.getMinutes() & 0x3f) << 5) |
|
|
77
|
+
((Math.floor(d.getSeconds() / 2)) & 0x1f);
|
|
78
|
+
var dosDate = (((d.getFullYear() - 1980) & 0x7f) << 9) |
|
|
79
|
+
(((d.getMonth() + 1) & 0xf) << 5) |
|
|
80
|
+
(d.getDate() & 0x1f);
|
|
81
|
+
return { time: dosTime, date: dosDate };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function zip() {
|
|
85
|
+
var entries = [];
|
|
86
|
+
|
|
87
|
+
function addFile(name, content, opts) {
|
|
88
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
89
|
+
throw new ArchiveError("archive/bad-name", "addFile: name must be a non-empty string");
|
|
90
|
+
}
|
|
91
|
+
if (name.indexOf("\0") !== -1) {
|
|
92
|
+
throw new ArchiveError("archive/bad-name", "addFile: name contains null byte");
|
|
93
|
+
}
|
|
94
|
+
// No path traversal — relative paths only, no leading slash, no ".." segments.
|
|
95
|
+
var normalized = name.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
96
|
+
var segs = normalized.split("/");
|
|
97
|
+
for (var si = 0; si < segs.length; si++) {
|
|
98
|
+
if (segs[si] === "..") {
|
|
99
|
+
throw new ArchiveError("archive/bad-name", "addFile: name contains '..' segment");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
var bodyBuf;
|
|
103
|
+
if (Buffer.isBuffer(content)) bodyBuf = content;
|
|
104
|
+
else if (typeof content === "string") bodyBuf = Buffer.from(content, "utf8");
|
|
105
|
+
else throw new ArchiveError("archive/bad-content",
|
|
106
|
+
"addFile: content must be a Buffer or string, got " + typeof content);
|
|
107
|
+
|
|
108
|
+
opts = opts || {};
|
|
109
|
+
var method = opts.method === "store" ? METHOD_STORE : METHOD_DEFLATE;
|
|
110
|
+
var mtime = opts.mtime instanceof Date ? opts.mtime : new Date();
|
|
111
|
+
|
|
112
|
+
var crc = _crc32(bodyBuf);
|
|
113
|
+
var stored = bodyBuf;
|
|
114
|
+
if (method === METHOD_DEFLATE) {
|
|
115
|
+
stored = zlib.deflateRawSync(bodyBuf);
|
|
116
|
+
// If deflate didn't shrink it (small/already-compressed inputs),
|
|
117
|
+
// fall back to STORE to save the operator a few bytes.
|
|
118
|
+
if (stored.length >= bodyBuf.length) {
|
|
119
|
+
stored = bodyBuf;
|
|
120
|
+
method = METHOD_STORE;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
entries.push({
|
|
125
|
+
name: normalized,
|
|
126
|
+
method: method,
|
|
127
|
+
mtime: mtime,
|
|
128
|
+
crc: crc,
|
|
129
|
+
stored: stored,
|
|
130
|
+
uncompressedSize: bodyBuf.length,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _buildLocalFileHeader(entry) {
|
|
135
|
+
var nameBuf = Buffer.from(entry.name, "utf8");
|
|
136
|
+
var dt = _msdosDateTime(entry.mtime);
|
|
137
|
+
var hdr = Buffer.alloc(30);
|
|
138
|
+
hdr.writeUInt32LE(SIG_LFH, 0);
|
|
139
|
+
hdr.writeUInt16LE(20, 4); // version needed
|
|
140
|
+
hdr.writeUInt16LE(0x0800, 6); // flags: bit 11 = UTF-8 name
|
|
141
|
+
hdr.writeUInt16LE(entry.method, 8);
|
|
142
|
+
hdr.writeUInt16LE(dt.time, 10);
|
|
143
|
+
hdr.writeUInt16LE(dt.date, 12);
|
|
144
|
+
hdr.writeUInt32LE(entry.crc, 14);
|
|
145
|
+
hdr.writeUInt32LE(entry.stored.length, 18);
|
|
146
|
+
hdr.writeUInt32LE(entry.uncompressedSize, 22);
|
|
147
|
+
hdr.writeUInt16LE(nameBuf.length, 26);
|
|
148
|
+
hdr.writeUInt16LE(0, 28); // extra field length
|
|
149
|
+
return Buffer.concat([hdr, nameBuf]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _buildCentralDirectoryEntry(entry, lfhOffset) {
|
|
153
|
+
var nameBuf = Buffer.from(entry.name, "utf8");
|
|
154
|
+
var dt = _msdosDateTime(entry.mtime);
|
|
155
|
+
var hdr = Buffer.alloc(46);
|
|
156
|
+
hdr.writeUInt32LE(SIG_CFH, 0);
|
|
157
|
+
hdr.writeUInt16LE(0x033f, 4); // version made by (UNIX | 6.3)
|
|
158
|
+
hdr.writeUInt16LE(20, 6); // version needed
|
|
159
|
+
hdr.writeUInt16LE(0x0800, 8); // flags: bit 11 = UTF-8
|
|
160
|
+
hdr.writeUInt16LE(entry.method, 10);
|
|
161
|
+
hdr.writeUInt16LE(dt.time, 12);
|
|
162
|
+
hdr.writeUInt16LE(dt.date, 14);
|
|
163
|
+
hdr.writeUInt32LE(entry.crc, 16);
|
|
164
|
+
hdr.writeUInt32LE(entry.stored.length, 20);
|
|
165
|
+
hdr.writeUInt32LE(entry.uncompressedSize, 24);
|
|
166
|
+
hdr.writeUInt16LE(nameBuf.length, 28);
|
|
167
|
+
hdr.writeUInt16LE(0, 30); // extra field length
|
|
168
|
+
hdr.writeUInt16LE(0, 32); // file comment length
|
|
169
|
+
hdr.writeUInt16LE(0, 34); // disk number start
|
|
170
|
+
hdr.writeUInt16LE(0, 36); // internal file attributes
|
|
171
|
+
hdr.writeUInt32LE(0, 38); // external file attributes
|
|
172
|
+
hdr.writeUInt32LE(lfhOffset, 42);
|
|
173
|
+
return Buffer.concat([hdr, nameBuf]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toBuffer() {
|
|
177
|
+
if (entries.length > 65535) {
|
|
178
|
+
throw new ArchiveError("archive/too-many-entries",
|
|
179
|
+
"ZIP archive cannot contain more than 65535 entries (ZIP64 unsupported in v1)");
|
|
180
|
+
}
|
|
181
|
+
var pieces = [];
|
|
182
|
+
var offsets = [];
|
|
183
|
+
var totalLocalBytes = 0;
|
|
184
|
+
for (var i = 0; i < entries.length; i++) {
|
|
185
|
+
offsets.push(totalLocalBytes);
|
|
186
|
+
var lfh = _buildLocalFileHeader(entries[i]);
|
|
187
|
+
pieces.push(lfh);
|
|
188
|
+
pieces.push(entries[i].stored);
|
|
189
|
+
totalLocalBytes += lfh.length + entries[i].stored.length;
|
|
190
|
+
}
|
|
191
|
+
var cdStart = totalLocalBytes;
|
|
192
|
+
var cdSize = 0;
|
|
193
|
+
for (var j = 0; j < entries.length; j++) {
|
|
194
|
+
var cdh = _buildCentralDirectoryEntry(entries[j], offsets[j]);
|
|
195
|
+
pieces.push(cdh);
|
|
196
|
+
cdSize += cdh.length;
|
|
197
|
+
}
|
|
198
|
+
// End of Central Directory
|
|
199
|
+
var eocd = Buffer.alloc(22);
|
|
200
|
+
eocd.writeUInt32LE(SIG_EOCD, 0);
|
|
201
|
+
eocd.writeUInt16LE(0, 4); // disk number
|
|
202
|
+
eocd.writeUInt16LE(0, 6); // disk where CD starts
|
|
203
|
+
eocd.writeUInt16LE(entries.length, 8); // entries on this disk
|
|
204
|
+
eocd.writeUInt16LE(entries.length, 10); // total entries
|
|
205
|
+
eocd.writeUInt32LE(cdSize, 12); // size of central directory
|
|
206
|
+
eocd.writeUInt32LE(cdStart, 16); // offset of central directory
|
|
207
|
+
eocd.writeUInt16LE(0, 20); // comment length
|
|
208
|
+
pieces.push(eocd);
|
|
209
|
+
return Buffer.concat(pieces);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeTo(filepath) {
|
|
213
|
+
var buf = toBuffer();
|
|
214
|
+
fs.writeFileSync(filepath, buf);
|
|
215
|
+
return buf.length;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function digest() {
|
|
219
|
+
// SHA-256 of the produced archive bytes — useful for operator-side
|
|
220
|
+
// integrity logging on exported bundles. Not vendor-locked to any
|
|
221
|
+
// particular hash; SHA-256 is universally recognized for content
|
|
222
|
+
// addressing.
|
|
223
|
+
return nodeCrypto.createHash("sha256").update(toBuffer()).digest("hex");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
addFile: addFile,
|
|
228
|
+
toBuffer: toBuffer,
|
|
229
|
+
writeTo: writeTo,
|
|
230
|
+
digest: digest,
|
|
231
|
+
get entryCount() { return entries.length; },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
zip: zip,
|
|
237
|
+
ArchiveError: ArchiveError,
|
|
238
|
+
// Test-only export — operators don't call this; it's here for unit-testing
|
|
239
|
+
// the CRC implementation against known vectors.
|
|
240
|
+
_crc32ForTest: _crc32,
|
|
241
|
+
};
|
package/lib/time.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* time — timezone-aware datetime arithmetic + formatting on top of
|
|
4
|
+
* native `Intl.DateTimeFormat`. No TZ-database vendor; operators get
|
|
5
|
+
* the IANA names Node's ICU build supports (full set on every
|
|
6
|
+
* mainstream platform).
|
|
7
|
+
*
|
|
8
|
+
* b.time.toParts(d, { timezone: "America/New_York" })
|
|
9
|
+
* → { year, month, day, hour, minute, second, millisecond,
|
|
10
|
+
* weekday: 1..7, weekdayName: "Mon"..."Sun", dayOfYear }
|
|
11
|
+
*
|
|
12
|
+
* b.time.format(d, { timezone, locale, dateStyle, timeStyle })
|
|
13
|
+
* → operator-readable string
|
|
14
|
+
*
|
|
15
|
+
* b.time.startOfDay(d, { timezone }) → midnight in TZ
|
|
16
|
+
* b.time.endOfDay(d, { timezone }) → 23:59:59.999 in TZ
|
|
17
|
+
* b.time.addDays(d, n, { timezone }) → calendar-day add (DST-safe)
|
|
18
|
+
* b.time.addMonths(d, n, { timezone }) → calendar-month add
|
|
19
|
+
* b.time.diffDays(a, b, { timezone }) → calendar days between
|
|
20
|
+
*
|
|
21
|
+
* b.time.parseISO(s) → Date | throws TimeError
|
|
22
|
+
* b.time.tzOffsetMs(d, timezone) → ms offset (= local - utc)
|
|
23
|
+
*
|
|
24
|
+
* All ops accept Date, ms-epoch number, or ISO 8601 string. `timezone`
|
|
25
|
+
* defaults to UTC. `locale` defaults to "en-US".
|
|
26
|
+
*/
|
|
27
|
+
var { defineClass } = require("./framework-error");
|
|
28
|
+
|
|
29
|
+
var TimeError = defineClass("TimeError", { alwaysPermanent: true });
|
|
30
|
+
|
|
31
|
+
var DEFAULT_TIMEZONE = "UTC";
|
|
32
|
+
var DEFAULT_LOCALE = "en-US";
|
|
33
|
+
|
|
34
|
+
var _dtfCache = new Map();
|
|
35
|
+
function _dtf(opts) {
|
|
36
|
+
var key = JSON.stringify(opts);
|
|
37
|
+
if (_dtfCache.has(key)) return _dtfCache.get(key);
|
|
38
|
+
var dtf;
|
|
39
|
+
try { dtf = new Intl.DateTimeFormat(opts.locale || DEFAULT_LOCALE, opts); }
|
|
40
|
+
catch (e) {
|
|
41
|
+
throw new TimeError("time/bad-timezone-or-locale",
|
|
42
|
+
"Intl rejected the timezone/locale: " + ((e && e.message) || String(e)));
|
|
43
|
+
}
|
|
44
|
+
_dtfCache.set(key, dtf);
|
|
45
|
+
return dtf;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _toDate(v) {
|
|
49
|
+
if (v instanceof Date) {
|
|
50
|
+
if (isNaN(v.getTime())) {
|
|
51
|
+
throw new TimeError("time/invalid-date", "input Date is invalid (NaN)");
|
|
52
|
+
}
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
if (typeof v === "number") {
|
|
56
|
+
if (!isFinite(v)) {
|
|
57
|
+
throw new TimeError("time/invalid-ms", "input must be a finite number of milliseconds");
|
|
58
|
+
}
|
|
59
|
+
return new Date(v);
|
|
60
|
+
}
|
|
61
|
+
if (typeof v === "string") return parseISO(v);
|
|
62
|
+
throw new TimeError("time/bad-input",
|
|
63
|
+
"expected Date | number | ISO string, got " + typeof v);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var WEEKDAY_TO_NUM = {
|
|
67
|
+
"Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6, "Sun": 7,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function toParts(input, opts) {
|
|
71
|
+
opts = opts || {};
|
|
72
|
+
var date = _toDate(input);
|
|
73
|
+
var tz = opts.timezone || DEFAULT_TIMEZONE;
|
|
74
|
+
var dtf = _dtf({
|
|
75
|
+
timeZone: tz,
|
|
76
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
77
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
78
|
+
weekday: "short",
|
|
79
|
+
hour12: false,
|
|
80
|
+
});
|
|
81
|
+
var parts = dtf.formatToParts(date);
|
|
82
|
+
var out = { millisecond: date.getUTCMilliseconds() };
|
|
83
|
+
for (var i = 0; i < parts.length; i++) {
|
|
84
|
+
var p = parts[i];
|
|
85
|
+
if (p.type === "year") out.year = parseInt(p.value, 10);
|
|
86
|
+
if (p.type === "month") out.month = parseInt(p.value, 10);
|
|
87
|
+
if (p.type === "day") out.day = parseInt(p.value, 10);
|
|
88
|
+
if (p.type === "hour") out.hour = (p.value === "24" ? 0 : parseInt(p.value, 10));
|
|
89
|
+
if (p.type === "minute") out.minute = parseInt(p.value, 10);
|
|
90
|
+
if (p.type === "second") out.second = parseInt(p.value, 10);
|
|
91
|
+
if (p.type === "weekday") {
|
|
92
|
+
out.weekdayName = p.value;
|
|
93
|
+
out.weekday = WEEKDAY_TO_NUM[p.value] || null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// dayOfYear: computed from out.year + out.month + out.day directly,
|
|
97
|
+
// no recursion through toParts. Days-in-month table for non-leap;
|
|
98
|
+
// Feb gets +1 in leap years (Gregorian rule: divisible by 4, not 100,
|
|
99
|
+
// unless 400).
|
|
100
|
+
var DAYS_BEFORE_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
|
101
|
+
var leap = (out.year % 4 === 0 && out.year % 100 !== 0) || (out.year % 400 === 0);
|
|
102
|
+
out.dayOfYear = DAYS_BEFORE_MONTH[out.month - 1] + out.day + (leap && out.month > 2 ? 1 : 0);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function format(input, opts) {
|
|
107
|
+
opts = opts || {};
|
|
108
|
+
var date = _toDate(input);
|
|
109
|
+
var fmtOpts = {
|
|
110
|
+
timeZone: opts.timezone || DEFAULT_TIMEZONE,
|
|
111
|
+
locale: opts.locale || DEFAULT_LOCALE,
|
|
112
|
+
};
|
|
113
|
+
if (opts.dateStyle) fmtOpts.dateStyle = opts.dateStyle;
|
|
114
|
+
if (opts.timeStyle) fmtOpts.timeStyle = opts.timeStyle;
|
|
115
|
+
var passthroughKeys = [
|
|
116
|
+
"year", "month", "day", "hour", "minute", "second",
|
|
117
|
+
"weekday", "era", "hour12", "fractionalSecondDigits",
|
|
118
|
+
"timeZoneName",
|
|
119
|
+
];
|
|
120
|
+
for (var i = 0; i < passthroughKeys.length; i++) {
|
|
121
|
+
var k = passthroughKeys[i];
|
|
122
|
+
if (opts[k] !== undefined) fmtOpts[k] = opts[k];
|
|
123
|
+
}
|
|
124
|
+
if (!opts.dateStyle && !opts.timeStyle && !passthroughKeys.some(function (k) { return opts[k] !== undefined; })) {
|
|
125
|
+
fmtOpts.dateStyle = "medium";
|
|
126
|
+
fmtOpts.timeStyle = "short";
|
|
127
|
+
}
|
|
128
|
+
return _dtf(fmtOpts).format(date);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function tzOffsetMs(input, timezone) {
|
|
132
|
+
var date = _toDate(input);
|
|
133
|
+
if (!timezone || typeof timezone !== "string") {
|
|
134
|
+
throw new TimeError("time/bad-timezone",
|
|
135
|
+
"tzOffsetMs: timezone must be a non-empty IANA name");
|
|
136
|
+
}
|
|
137
|
+
var dtf = _dtf({
|
|
138
|
+
timeZone: timezone,
|
|
139
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
140
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
141
|
+
hour12: false,
|
|
142
|
+
});
|
|
143
|
+
var parts = {};
|
|
144
|
+
dtf.formatToParts(date).forEach(function (p) { parts[p.type] = p.value; });
|
|
145
|
+
var hour = parts.hour === "24" ? "00" : parts.hour;
|
|
146
|
+
var asUtcMs = Date.UTC(
|
|
147
|
+
parseInt(parts.year, 10),
|
|
148
|
+
parseInt(parts.month, 10) - 1,
|
|
149
|
+
parseInt(parts.day, 10),
|
|
150
|
+
parseInt(hour, 10),
|
|
151
|
+
parseInt(parts.minute, 10),
|
|
152
|
+
parseInt(parts.second, 10)
|
|
153
|
+
);
|
|
154
|
+
var instantSec = Math.floor(date.getTime() / 1000) * 1000;
|
|
155
|
+
return asUtcMs - instantSec;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _fromPartsAtTz(p, timezone) {
|
|
159
|
+
var candidate = Date.UTC(
|
|
160
|
+
p.year,
|
|
161
|
+
(p.month - 1),
|
|
162
|
+
p.day,
|
|
163
|
+
p.hour || 0,
|
|
164
|
+
p.minute || 0,
|
|
165
|
+
p.second || 0,
|
|
166
|
+
p.millisecond || 0
|
|
167
|
+
);
|
|
168
|
+
var offset1 = tzOffsetMs(candidate, timezone);
|
|
169
|
+
var step1 = candidate - offset1;
|
|
170
|
+
var offset2 = tzOffsetMs(step1, timezone);
|
|
171
|
+
return new Date(step1 - (offset2 - offset1));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function startOfDay(input, opts) {
|
|
175
|
+
opts = opts || {};
|
|
176
|
+
var tz = opts.timezone || DEFAULT_TIMEZONE;
|
|
177
|
+
var p = toParts(input, { timezone: tz });
|
|
178
|
+
return _fromPartsAtTz({
|
|
179
|
+
year: p.year, month: p.month, day: p.day,
|
|
180
|
+
hour: 0, minute: 0, second: 0, millisecond: 0,
|
|
181
|
+
}, tz);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function endOfDay(input, opts) {
|
|
185
|
+
opts = opts || {};
|
|
186
|
+
var tz = opts.timezone || DEFAULT_TIMEZONE;
|
|
187
|
+
var p = toParts(input, { timezone: tz });
|
|
188
|
+
return _fromPartsAtTz({
|
|
189
|
+
year: p.year, month: p.month, day: p.day,
|
|
190
|
+
hour: 23, minute: 59, second: 59, millisecond: 999,
|
|
191
|
+
}, tz);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function addDays(input, n, opts) {
|
|
195
|
+
opts = opts || {};
|
|
196
|
+
if (typeof n !== "number" || !isFinite(n)) {
|
|
197
|
+
throw new TimeError("time/bad-arg", "addDays: n must be a finite number");
|
|
198
|
+
}
|
|
199
|
+
var tz = opts.timezone || DEFAULT_TIMEZONE;
|
|
200
|
+
var p = toParts(input, { timezone: tz });
|
|
201
|
+
var asUtc = new Date(Date.UTC(p.year, p.month - 1, p.day + Math.trunc(n),
|
|
202
|
+
p.hour, p.minute, p.second, p.millisecond));
|
|
203
|
+
return _fromPartsAtTz({
|
|
204
|
+
year: asUtc.getUTCFullYear(),
|
|
205
|
+
month: asUtc.getUTCMonth() + 1,
|
|
206
|
+
day: asUtc.getUTCDate(),
|
|
207
|
+
hour: p.hour, minute: p.minute, second: p.second, millisecond: p.millisecond,
|
|
208
|
+
}, tz);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function addMonths(input, n, opts) {
|
|
212
|
+
opts = opts || {};
|
|
213
|
+
if (typeof n !== "number" || !isFinite(n)) {
|
|
214
|
+
throw new TimeError("time/bad-arg", "addMonths: n must be a finite number");
|
|
215
|
+
}
|
|
216
|
+
var tz = opts.timezone || DEFAULT_TIMEZONE;
|
|
217
|
+
var p = toParts(input, { timezone: tz });
|
|
218
|
+
var newMonth0 = (p.month - 1) + Math.trunc(n);
|
|
219
|
+
var newYear = p.year + Math.floor(newMonth0 / 12);
|
|
220
|
+
newMonth0 = ((newMonth0 % 12) + 12) % 12;
|
|
221
|
+
var daysInNew = new Date(Date.UTC(newYear, newMonth0 + 1, 0)).getUTCDate();
|
|
222
|
+
var newDay = Math.min(p.day, daysInNew);
|
|
223
|
+
return _fromPartsAtTz({
|
|
224
|
+
year: newYear, month: newMonth0 + 1, day: newDay,
|
|
225
|
+
hour: p.hour, minute: p.minute, second: p.second, millisecond: p.millisecond,
|
|
226
|
+
}, tz);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function diffDays(a, b, opts) {
|
|
230
|
+
opts = opts || {};
|
|
231
|
+
var tz = opts.timezone || DEFAULT_TIMEZONE;
|
|
232
|
+
var aMid = startOfDay(a, { timezone: tz });
|
|
233
|
+
var bMid = startOfDay(b, { timezone: tz });
|
|
234
|
+
return Math.round((bMid.getTime() - aMid.getTime()) / 86400000);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
238
|
+
|
|
239
|
+
function parseISO(s) {
|
|
240
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
241
|
+
throw new TimeError("time/bad-iso", "parseISO: input must be a non-empty string");
|
|
242
|
+
}
|
|
243
|
+
var m = ISO_RE.exec(s);
|
|
244
|
+
if (!m) {
|
|
245
|
+
throw new TimeError("time/bad-iso",
|
|
246
|
+
"parseISO: not an ISO 8601 datetime: " + JSON.stringify(s));
|
|
247
|
+
}
|
|
248
|
+
var year = parseInt(m[1], 10);
|
|
249
|
+
var month = parseInt(m[2], 10);
|
|
250
|
+
var day = parseInt(m[3], 10);
|
|
251
|
+
var hour = m[4] ? parseInt(m[4], 10) : 0;
|
|
252
|
+
var minute = m[5] ? parseInt(m[5], 10) : 0;
|
|
253
|
+
var second = m[6] ? parseInt(m[6], 10) : 0;
|
|
254
|
+
var msStr = m[7] || "";
|
|
255
|
+
var ms = msStr ? parseInt((msStr + "000").slice(0, 3), 10) : 0;
|
|
256
|
+
var tz = m[8];
|
|
257
|
+
|
|
258
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 ||
|
|
259
|
+
hour > 23 || minute > 59 || second > 59) {
|
|
260
|
+
throw new TimeError("time/bad-iso",
|
|
261
|
+
"parseISO: out-of-range component in " + JSON.stringify(s));
|
|
262
|
+
}
|
|
263
|
+
var utcMs;
|
|
264
|
+
if (!tz) {
|
|
265
|
+
utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms);
|
|
266
|
+
} else if (tz === "Z") {
|
|
267
|
+
utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms);
|
|
268
|
+
} else {
|
|
269
|
+
var sign = tz.charAt(0) === "-" ? -1 : 1;
|
|
270
|
+
var hh = parseInt(tz.slice(1, 3), 10);
|
|
271
|
+
var mm = parseInt(tz.slice(tz.length - 2), 10);
|
|
272
|
+
var offsetMs = sign * (hh * 3600 + mm * 60) * 1000;
|
|
273
|
+
utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms) - offsetMs;
|
|
274
|
+
}
|
|
275
|
+
return new Date(utcMs);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
toParts: toParts,
|
|
280
|
+
format: format,
|
|
281
|
+
tzOffsetMs: tzOffsetMs,
|
|
282
|
+
startOfDay: startOfDay,
|
|
283
|
+
endOfDay: endOfDay,
|
|
284
|
+
addDays: addDays,
|
|
285
|
+
addMonths: addMonths,
|
|
286
|
+
diffDays: diffDays,
|
|
287
|
+
parseISO: parseISO,
|
|
288
|
+
TimeError: TimeError,
|
|
289
|
+
};
|