w-orm-reladb 1.1.13 → 1.1.14
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 +23 -0
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +14 -14
- package/docs/WOrmReladb.mjs.html +15 -5
- package/docs/cvFind.mjs.html +100 -7
- package/docs/global.html +185 -12
- package/docs/importModels.mjs.html +2 -2
- package/docs/index.html +2 -2
- package/models/tfloat.js +20 -0
- package/models/tlower.js +38 -0
- package/package.json +2 -2
- package/src/WOrmReladb.mjs +13 -3
- package/src/cvFind.mjs +98 -5
- package/test/api-float.mjs +262 -0
- package/test/api-lower-cases.mjs +129 -0
- package/test/api-mssql.test.mjs +6 -0
- package/test/api-scope.mjs +208 -0
- package/test/api-sqlite.test.mjs +6 -0
- package/test/tools/floatTools.mjs +117 -0
- package/test/unit-cvfind.test.mjs +11 -2
- package/test/unit-float.test.mjs +492 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//floatTools, 浮動時間(ctx.dateNoTz:'floating'、toFloating; CLAUDE_rulebook.md之R14)測試共用之預言機與輔助, 由unit-float.test.mjs與api-float.mjs引入
|
|
2
|
+
//預言機一律獨立於被測碼與process.env.TZ之本地getter: 以Intl.DateTimeFormat(timeZone: 名稱, ICU)取瞬間於目標時區之牆上分量; 字串形式之牆上分量即UTC分量
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
//時區矩陣, 10個: UTC、UTC+8、+5:30、+5:45、+12:45/+13:45(Chatham)、+14、美西(夏令)、美東(夏令)、聖保羅(-3)、-12
|
|
7
|
+
export let TZS = ['UTC', 'Asia/Taipei', 'Asia/Kolkata', 'Asia/Kathmandu', 'Pacific/Chatham', 'Pacific/Kiritimati', 'America/Los_Angeles', 'America/New_York', 'America/Sao_Paulo', 'Etc/GMT+12']
|
|
8
|
+
|
|
9
|
+
//有夏令之UTC+時區(柏林: 2026-03-29 02:00–03:00不存在; Lord Howe: 2026-10-04 02:00–02:30不存在, 半小時跳時), 供api層矩陣
|
|
10
|
+
export let TZS_DST_EAST = ['Europe/Berlin', 'Australia/Lord_Howe']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export let pad = (n, w = 2) => String(n).padStart(w, '0')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
//utcOf, 分量[y,mo,d,h,mi,s,ms] -> 以UTC分量建構之Date(setUTCFullYear避開Date.UTC對0–99年之映射)
|
|
17
|
+
export let utcOf = (c) => {
|
|
18
|
+
let t = new Date(0)
|
|
19
|
+
t.setUTCFullYear(c[0], c[1] - 1, c[2])
|
|
20
|
+
t.setUTCHours(c[3], c[4], c[5], c[6])
|
|
21
|
+
return t
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//wallIntl, 瞬間(毫秒)於時區tz之牆上分量[y,mo,d,h,mi,s,ms](ICU; 西元前以era換算)
|
|
25
|
+
export let wallIntl = (ms, tz) => {
|
|
26
|
+
let f = new Intl.DateTimeFormat('en-US', { timeZone: tz, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', era: 'short' })
|
|
27
|
+
let o = {}
|
|
28
|
+
for (let x of f.formatToParts(new Date(ms))) {
|
|
29
|
+
o[x.type] = x.value
|
|
30
|
+
}
|
|
31
|
+
let y = Number(o.year)
|
|
32
|
+
if (o.era === 'BC' || o.era === 'B') {
|
|
33
|
+
y = 1 - y
|
|
34
|
+
}
|
|
35
|
+
let msPart = ((ms % 1000) + 1000) % 1000
|
|
36
|
+
return [y, Number(o.month), Number(o.day), Number(o.hour), Number(o.minute), Number(o.second), msPart]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//offIntl, 瞬間於時區tz之偏移(分鐘, UTC減本地, 與Date.getTimezoneOffset同號)
|
|
40
|
+
export let offIntl = (ms, tz) => Math.round((ms - utcOf(wallIntl(ms, tz)).getTime()) / 60000)
|
|
41
|
+
|
|
42
|
+
//tzSelfCheck, 切換時區後自證: 本地getter之偏移(冬、夏兩個日期)須與Intl之偏移一致, 否則時區切換未生效(如Windows啟動時給IANA名不生效), 測試無意義
|
|
43
|
+
export let tzSelfCheck = (tz) => {
|
|
44
|
+
let sum = new Date(2026, 6, 1).getTimezoneOffset()
|
|
45
|
+
let win = new Date(2026, 0, 1).getTimezoneOffset()
|
|
46
|
+
assert.strictEqual(sum, offIntl(Date.UTC(2026, 6, 1, 0, 0, 0), tz), `${tz}: 時區切換未生效(夏季偏移)`)
|
|
47
|
+
assert.strictEqual(win, offIntl(Date.UTC(2026, 0, 1, 0, 0, 0), tz), `${tz}: 時區切換未生效(冬季偏移)`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//inYear, 浮動時間之牆上年須在1–9999
|
|
51
|
+
export let inYear = (y) => y >= 1 && y <= 9999
|
|
52
|
+
|
|
53
|
+
//wallNum, 牆上時間字串(YYYY-MM-DD[T 或空白]HH:mm:ss)-> 以UTC分量表示之整數, 與主機時區無關(Jet語意: 無時區、無夏令, 供比較)
|
|
54
|
+
export let wallNum = (s) => {
|
|
55
|
+
let m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})$/)
|
|
56
|
+
assert.ok(m !== null, `牆上時間格式: ${s}`)
|
|
57
|
+
return utcOf([Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]), 0]).getTime()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//mkLocal, 以本地分量建構Date(含年0–99之完整年份), 夏令跳過之時刻由JS正規化
|
|
61
|
+
export let mkLocal = (y, mo, d, h = 0, mi = 0, s = 0) => {
|
|
62
|
+
let t = new Date(2000, 0, 1, 0, 0, 0, 0)
|
|
63
|
+
t.setFullYear(y, mo - 1, d)
|
|
64
|
+
t.setHours(h, mi, s, 0)
|
|
65
|
+
return t
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//wallOfDate, Date之本地分量 -> 牆上時間字串(Date/epoch條件於Jet語意下之牆上時間)
|
|
69
|
+
export let wallOfDate = (d) => `${pad(d.getFullYear(), 4)}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
70
|
+
|
|
71
|
+
//cmpOk, 比較運算子的預言機
|
|
72
|
+
export let cmpOk = (op, a, b) => {
|
|
73
|
+
return op === '$lt' ? a < b : op === '$lte' ? a <= b : op === '$gt' ? a > b : op === '$gte' ? a >= b : op === '$eq' ? a === b : a !== b
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export let OPS = ['$lt', '$lte', '$gt', '$gte', '$eq', '$ne']
|
|
77
|
+
|
|
78
|
+
//WALL, Jet之牆上時間(固定格式字串; null為NULL列): 哨兵9999、年0100、美東夏令跳過/重疊、柏林與Lord Howe之夏令跳過
|
|
79
|
+
export let WALL = [
|
|
80
|
+
'2020-01-01 00:00:00', '2026-10-19 23:59:59', '2026-10-20 00:00:00', '2026-10-20 12:34:56', '2026-12-31 23:59:59', '2027-01-01 00:00:00', '2099-01-01 00:00:00',
|
|
81
|
+
'9999-12-31 00:00:00', '9999-12-31 12:00:00', '9999-12-31 23:59:59', '0100-01-01 00:00:00', '0100-01-01 12:00:00',
|
|
82
|
+
'2026-03-08 01:59:59', '2026-03-08 03:00:00', '2026-11-01 00:59:59', '2026-11-01 01:30:00', '2026-11-01 02:00:00',
|
|
83
|
+
'2026-03-29 02:30:00', '2026-10-04 02:15:00', null,
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
//CONDS, 條件值(牆上時間意義[牆上時間, 值形式]): 字串(無時區)、本地分量建構之Date、其epoch
|
|
87
|
+
export let CONDS = [
|
|
88
|
+
['2026-10-20 00:00:00', 'str'], ['9999-12-31 23:59:59', 'str'], ['2020-01-01 00:00:00', 'str'], ['2099-01-01 00:00:00', 'str'], ['0100-01-01 00:00:00', 'str'],
|
|
89
|
+
['2026-03-08 02:30:00', 'str'], ['2026-11-01 01:30:00', 'str'], ['9999-12-31 00:00:00', 'str'], ['2026-12-31 23:59:59', 'str'],
|
|
90
|
+
['2026-10-20 00:00:00', 'date'], ['9999-12-31 23:59:59', 'date'], ['2020-01-01 00:00:00', 'date'], ['0100-01-01 00:00:00', 'date'], ['2026-11-01 01:30:00', 'date'],
|
|
91
|
+
['2026-10-20 00:00:00', 'epoch'], ['9999-12-31 23:59:59', 'epoch'],
|
|
92
|
+
['2026-03-29 03:00:00', 'str'], ['2026-10-04 02:30:00', 'str'],
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
//condVal, 依值形式產生條件值(Date/epoch以本地分量建構, 須於withTz內呼叫)
|
|
96
|
+
export let condVal = (w, kind) => {
|
|
97
|
+
if (kind === 'str') {
|
|
98
|
+
return w.replace(' ', 'T')
|
|
99
|
+
}
|
|
100
|
+
let m = w.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/)
|
|
101
|
+
let d = mkLocal(Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]))
|
|
102
|
+
return kind === 'date' ? d : d.getTime()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//condWall, 預言機之條件牆上時間: 字串即其自身; Date/epoch為其本地分量(夏令跳過之時刻mkLocal已被JS正規化, 以結果Date之本地分量為準)
|
|
106
|
+
export let condWall = (w, kind) => {
|
|
107
|
+
if (kind === 'str') {
|
|
108
|
+
return w
|
|
109
|
+
}
|
|
110
|
+
let v = condVal(w, kind)
|
|
111
|
+
return wallOfDate(kind === 'date' ? v : new Date(v))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//wantIdx, 預言機: 牆上時間數值比較(Jet語意), 回傳命中之WALL索引
|
|
115
|
+
export let wantIdx = (op, cw) => {
|
|
116
|
+
return WALL.map((x, i) => [x, i]).filter(([x]) => x !== null && cmpOk(op, wallNum(x), wallNum(cw))).map(([, i]) => i)
|
|
117
|
+
}
|
|
@@ -717,11 +717,12 @@ describe('cvFind 審計02', function() {
|
|
|
717
717
|
}
|
|
718
718
|
})
|
|
719
719
|
|
|
720
|
-
it('[R4 dateNoTz] 值域: undefined
|
|
720
|
+
it('[R4 dateNoTz] 值域: undefined、\'local\'或\'floating\'(R14), 其餘拋錯(含find為空或非物件時)', function() {
|
|
721
721
|
assert.doesNotThrow(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...LT.ctx, dateNoTz: undefined }))
|
|
722
722
|
assert.doesNotThrow(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...LT.ctx, dateNoTz: 'local' }))
|
|
723
|
+
assert.doesNotThrow(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...LT.ctx, dateNoTz: 'floating' }))
|
|
723
724
|
for (let v of ['Local', 'utc', 'UTC', null, false, true, '', 0, {}]) {
|
|
724
|
-
throwsMsg(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...LT.ctx, dateNoTz: v }), 'invalid ctx: dateNoTz must be \'local\' or undefined')
|
|
725
|
+
throwsMsg(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...LT.ctx, dateNoTz: v }), 'invalid ctx: dateNoTz must be \'local\', \'floating\' or undefined')
|
|
725
726
|
}
|
|
726
727
|
//驗證早於提前返回, 拼錯不會被靜默忽略
|
|
727
728
|
throwsMsg(() => cvFind({}, { ...LT.ctx, dateNoTz: 'Local' }), 'invalid ctx')
|
|
@@ -1059,6 +1060,14 @@ describe('cvFind 第33項批1(R11 系列條件白名單接線)', function() {
|
|
|
1059
1060
|
assert.deepStrictEqual(cvFind(Object.create(null), {}), {})
|
|
1060
1061
|
})
|
|
1061
1062
|
|
|
1063
|
+
it('[R16] where頂層恆為單一Op.and(縱深防線: 純鍵與Op.or之defaultScope不被Object.assign覆寫; 主防線為whereMergeStrategy:\'and\')', function() {
|
|
1064
|
+
for (let find of [{ s: 'a' }, { i: { $gt: 1 } }, { s: 'a', i: 1 }, { $or: [{ s: 'a' }, { i: 1 }] }, { $and: [{ s: 'a' }] }, { $or: [{ s: 'a' }] }, { s: { $in: [] } }, { s: { $in: ['a', 'b'] } }, { d: null }, { s: 'a', $or: [{ i: 1 }, { i: 2 }] }]) {
|
|
1065
|
+
let w = cvFind(find, LT.ctx)
|
|
1066
|
+
assert.deepStrictEqual(Reflect.ownKeys(w), [Op.and], `頂層鍵: ${JSON.stringify(find)}`)
|
|
1067
|
+
assert.ok(Array.isArray(w[Op.and]) && w[Op.and].length >= 1)
|
|
1068
|
+
}
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1062
1071
|
it('$and/$or清單與並列欄位之隱含AND分組: 項數超過4者遞迴二分為平衡樹, 項數與順序不變, 邏輯節點層數僅隨項數對數成長(4項以內維持平鋪)', function() {
|
|
1063
1072
|
//leaves, 逐層展開僅含單一Op.and/Op.or鍵之節點取葉, mx.v記最深邏輯節點層數
|
|
1064
1073
|
let leaves = (o, acc, lv, mx) => {
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import vm from 'vm'
|
|
3
|
+
import Sequelize from 'sequelize'
|
|
4
|
+
import cvFind, { toFloating } from '../src/cvFind.mjs'
|
|
5
|
+
import { withTz } from './tools/withTz.mjs'
|
|
6
|
+
import { TZS, TZS_DST_EAST, pad, utcOf, wallIntl, tzSelfCheck, inYear } from './tools/floatTools.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
//第87項批B1(CLAUDE_rulebook.md之R14): ctx.dateNoTz:'floating'與匯出toFloating, 本檔不連線資料庫
|
|
10
|
+
// toFloating輸入空間表: 10個時區各146格(值形式10類, 邊界牆上時間11個) = 1460格, 預言機為Intl(ICU), 獨立於process.env.TZ之本地getter; 每時區先自證偏移
|
|
11
|
+
// cvFind之值域、語意、與'local'之差異、契約①(編碼後之浮動Date不可再當輸入)
|
|
12
|
+
//命中集合之語意(時區矩陣、哨兵、契約②④之混用)由test/api-float.mjs於sqlite與mssql實際執行斷言
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
let DT = Sequelize.DataTypes
|
|
16
|
+
|
|
17
|
+
let md = (() => {
|
|
18
|
+
let sq = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
19
|
+
let m = sq.define('tfl', {
|
|
20
|
+
id: { type: DT.STRING, primaryKey: true },
|
|
21
|
+
d: DT.DATE,
|
|
22
|
+
o: DT.DATEONLY,
|
|
23
|
+
s: DT.STRING,
|
|
24
|
+
i: DT.INTEGER,
|
|
25
|
+
}, { tableName: 'tfl', timestamps: false })
|
|
26
|
+
let qg = sq.getQueryInterface().queryGenerator
|
|
27
|
+
let ctx = { attrs: m.rawAttributes, pk: 'id' }
|
|
28
|
+
//where, 取實際送出之WHERE文字(DATE值為UTC文字, 格式 YYYY-MM-DD HH:mm:ss.SSS +00:00)
|
|
29
|
+
let where = (find, ex = {}) => {
|
|
30
|
+
let sql = qg.selectQuery('tfl', { where: cvFind(find, { ...ctx, ...ex }) }, m)
|
|
31
|
+
let k = sql.indexOf(' WHERE ')
|
|
32
|
+
return k >= 0 ? sql.slice(k + 7).replace(/;$/, '') : ''
|
|
33
|
+
}
|
|
34
|
+
return { ctx, where }
|
|
35
|
+
})()
|
|
36
|
+
|
|
37
|
+
let throwsMsg = (fn, msg) => {
|
|
38
|
+
let e = null
|
|
39
|
+
try {
|
|
40
|
+
fn()
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
e = err
|
|
44
|
+
}
|
|
45
|
+
assert.ok(e !== null, `未拋錯, 應含[${msg}]`)
|
|
46
|
+
assert.ok(String(e.message).indexOf(msg) >= 0, `訊息不含[${msg}]: ${e.message}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//ut, Date -> WHERE文字內之UTC時間文字
|
|
50
|
+
let ut = (d) => {
|
|
51
|
+
let s = d.toISOString()
|
|
52
|
+
return `${s.slice(0, 10)} ${s.slice(11, 23)} +00:00`
|
|
53
|
+
}
|
|
54
|
+
let isoOf = (c, sep = 'T', withMs = false) => `${pad(c[0], 4)}-${pad(c[1])}-${pad(c[2])}${sep}${pad(c[3])}:${pad(c[4])}:${pad(c[5])}${withMs ? `.${pad(c[6], 3)}` : ''}`
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
//邊界牆上時間(y,mo,d,h,mi,s,ms)
|
|
58
|
+
let WALLS = [
|
|
59
|
+
[1, 1, 1, 0, 0, 0, 0], [99, 12, 31, 23, 59, 59, 0], [100, 1, 1, 0, 0, 0, 0], [1970, 1, 1, 0, 0, 0, 0], [2024, 2, 29, 12, 0, 0, 0], [2026, 2, 28, 23, 59, 59, 999],
|
|
60
|
+
[2026, 3, 8, 2, 30, 0, 0], [2026, 11, 1, 1, 30, 0, 0], [2026, 10, 20, 12, 34, 56, 0], [9999, 12, 31, 0, 0, 0, 0], [9999, 12, 31, 23, 59, 59, 999],
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
//非法字串16種(皆須null): 純日期、曆法不存在、24時、60分、60秒、前後空白、小寫t、5位年、年0、亂字串、空字串、不完整偏移、斜線格式
|
|
64
|
+
let BAD_STR = ['2026-06-15', '2026-02-30T00:00:00', '2025-02-29T00:00:00', '2026-13-01T00:00:00', '2026-01-01T24:00:00', '2026-01-01T00:60:00', '2026-01-01T00:00:60', ' 2026-01-01T00:00:00', '2026-01-01T00:00:00 ', '2026-01-01t00:00:00', '10000-01-01T00:00:00', '0000-01-01T00:00:00', 'garbage', '', '2026-01-01T00:00:00+8', '2026/01/01 00:00:00']
|
|
65
|
+
|
|
66
|
+
//非法值(含Invalid Date共10種, 皆須null且不拋錯)
|
|
67
|
+
let BAD_VAL = [NaN, Infinity, 8.64e15 + 1, null, undefined, true, {}, [], 5n, new Date(NaN)]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
describe('toFloating: 輸入空間表(值形式 x 邊界牆上時間 x 時區)', function() {
|
|
71
|
+
|
|
72
|
+
let cells = 0
|
|
73
|
+
let byForm = {}
|
|
74
|
+
|
|
75
|
+
//check, 單格比對: want為null時須嚴格為null
|
|
76
|
+
let check = (form, tz, label, got, want) => {
|
|
77
|
+
cells += 1
|
|
78
|
+
byForm[form] = (byForm[form] || 0) + 1
|
|
79
|
+
if (want === null) {
|
|
80
|
+
assert.strictEqual(got, null, `${tz} ${form} [${label}] 應為null, 得 ${got === null ? null : got.toISOString()}`)
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
assert.ok(got instanceof Date, `${tz} ${form} [${label}] 應為Date, 得 ${String(got)}`)
|
|
84
|
+
assert.strictEqual(got.getTime(), want.getTime(), `${tz} ${form} [${label}] 得 ${got.toISOString()} 應為 ${want.toISOString()}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let tz of TZS) {
|
|
89
|
+
|
|
90
|
+
it(`${tz} 時區切換自證: 本地getter之偏移 = Intl之偏移`, function() {
|
|
91
|
+
withTz(tz, () => tzSelfCheck(tz))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it(`${tz} 無時區字串(T分隔、單一空白分隔、含毫秒、省略秒) x 11個牆上時間: 牆上分量即UTC分量, 與時區無關`, function() {
|
|
95
|
+
withTz(tz, () => {
|
|
96
|
+
for (let w of WALLS) {
|
|
97
|
+
let L = isoOf(w, 'T', true)
|
|
98
|
+
check('str-T', tz, L, toFloating(isoOf(w, 'T')), utcOf([w[0], w[1], w[2], w[3], w[4], w[5], 0]))
|
|
99
|
+
check('str-space', tz, L, toFloating(isoOf(w, ' ')), utcOf([w[0], w[1], w[2], w[3], w[4], w[5], 0]))
|
|
100
|
+
check('str-ms', tz, L, toFloating(isoOf(w, 'T', true)), utcOf(w))
|
|
101
|
+
check('str-nosec', tz, L, toFloating(`${pad(w[0], 4)}-${pad(w[1])}-${pad(w[2])}T${pad(w[3])}:${pad(w[4])}`), utcOf([w[0], w[1], w[2], w[3], w[4], 0, 0]))
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it(`${tz} Date、epoch、帶Z字串、帶偏移字串 x 11個牆上時間: 取瞬間於主機時區之牆上分量(預言機Intl), 牆上年不在1–9999者為null`, function() {
|
|
107
|
+
withTz(tz, () => {
|
|
108
|
+
for (let w of WALLS) {
|
|
109
|
+
let base = utcOf(w).getTime()
|
|
110
|
+
for (let inst of [base, base + 3600000 * 5]) {
|
|
111
|
+
let wi = wallIntl(inst, tz)
|
|
112
|
+
let want = inYear(wi[0]) ? utcOf(wi) : null
|
|
113
|
+
check('Date', tz, new Date(inst).toISOString(), toFloating(new Date(inst)), want)
|
|
114
|
+
check('epoch', tz, String(inst), toFloating(inst), want)
|
|
115
|
+
let z = new Date(inst).toISOString()
|
|
116
|
+
if (/^\d{4}-/.test(z)) {
|
|
117
|
+
check('str-Z', tz, z, toFloating(z), want)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//帶偏移字串(+05:45): 瞬間 = 牆上分量 - 偏移(字串不含毫秒)
|
|
121
|
+
let inst2 = utcOf([w[0], w[1], w[2], w[3], w[4], w[5], 0]).getTime() - (5 * 60 + 45) * 60000
|
|
122
|
+
let wi2 = wallIntl(inst2, tz)
|
|
123
|
+
check('str-offset', tz, `${isoOf(w, 'T')}+05:45`, toFloating(`${isoOf(w, 'T')}+05:45`), inYear(wi2[0]) ? utcOf(wi2) : null)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it(`${tz} 非法字串16種與非法值10種: 皆嚴格回null, 不拋錯`, function() {
|
|
129
|
+
withTz(tz, () => {
|
|
130
|
+
for (let bad of BAD_STR) {
|
|
131
|
+
check('invalid-string', tz, bad, toFloating(bad), null)
|
|
132
|
+
}
|
|
133
|
+
for (let bad of BAD_VAL) {
|
|
134
|
+
let got
|
|
135
|
+
assert.doesNotThrow(() => {
|
|
136
|
+
got = toFloating(bad)
|
|
137
|
+
}, `非法值不得拋錯: ${typeof bad === 'bigint' ? `${bad}n` : String(bad)}`)
|
|
138
|
+
check('invalid-value', tz, typeof bad === 'bigint' ? `${bad}n` : String(bad), got, null)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
it('空間表規模: 1460格(10個時區各146格), 各類格數如下', function() {
|
|
146
|
+
assert.strictEqual(TZS.length, 10)
|
|
147
|
+
assert.deepStrictEqual(byForm, {
|
|
148
|
+
'str-T': 110,
|
|
149
|
+
'str-space': 110,
|
|
150
|
+
'str-ms': 110,
|
|
151
|
+
'str-nosec': 110,
|
|
152
|
+
'Date': 220,
|
|
153
|
+
'epoch': 220,
|
|
154
|
+
'str-Z': 210,
|
|
155
|
+
'str-offset': 110,
|
|
156
|
+
'invalid-string': 160,
|
|
157
|
+
'invalid-value': 100,
|
|
158
|
+
})
|
|
159
|
+
assert.strictEqual(cells, 1460)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
describe('toFloating: 回傳值性質', function() {
|
|
166
|
+
|
|
167
|
+
it('全域改寫Date.prototype之getTime與本地分量getter不影響toFloating(以載入時取得之內建函數call呼叫; 外審minimax F5)', function() {
|
|
168
|
+
let names = ['getTime', 'getFullYear', 'getMonth', 'getDate', 'getHours', 'getMinutes', 'getSeconds', 'getMilliseconds']
|
|
169
|
+
let orig = {}
|
|
170
|
+
for (let n of names) {
|
|
171
|
+
orig[n] = Date.prototype[n]
|
|
172
|
+
}
|
|
173
|
+
let inputs = [() => new Date('2026-01-01T12:00:00Z'), () => 1767225600000, () => '2026-01-01T12:00:00Z', () => '2026-01-01T12:00:00']
|
|
174
|
+
for (let tz of ['UTC', 'Asia/Taipei', 'America/Los_Angeles']) {
|
|
175
|
+
withTz(tz, () => {
|
|
176
|
+
let want = inputs.map((mk) => toFloating(mk()).valueOf())
|
|
177
|
+
try {
|
|
178
|
+
for (let n of names) {
|
|
179
|
+
Reflect.defineProperty(Date.prototype, n, { value: () => 2099, writable: true, configurable: true })
|
|
180
|
+
}
|
|
181
|
+
let got = inputs.map((mk) => toFloating(mk()))
|
|
182
|
+
assert.ok(got.every((v) => v !== null), `${tz}: 改寫原型getter後得null`)
|
|
183
|
+
assert.deepStrictEqual(got.map((v) => v.valueOf()), want, `${tz}: 改寫原型getter後結果改變`)
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
for (let n of names) {
|
|
187
|
+
Reflect.defineProperty(Date.prototype, n, { value: orig[n], writable: true, configurable: true })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
for (let n of names) {
|
|
193
|
+
assert.strictEqual(Date.prototype[n], orig[n], `${n}未還原`)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('每次回傳新Date; 不改動輸入Date', function() {
|
|
198
|
+
let d = new Date(2026, 0, 1, 12, 0, 0)
|
|
199
|
+
let t0 = d.getTime()
|
|
200
|
+
let a = toFloating(d)
|
|
201
|
+
let b = toFloating(d)
|
|
202
|
+
assert.notStrictEqual(a, b)
|
|
203
|
+
assert.notStrictEqual(a, d)
|
|
204
|
+
assert.strictEqual(d.getTime(), t0)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('敵意輸入不拋錯、回null: valueOf/toString/getter拋錯之物件、Symbol、包裝物件、Proxy、無原型物件', function() {
|
|
208
|
+
let boom = () => {
|
|
209
|
+
throw new Error('boom')
|
|
210
|
+
}
|
|
211
|
+
let hostile = [
|
|
212
|
+
{ valueOf: boom },
|
|
213
|
+
{ toString: boom },
|
|
214
|
+
Symbol('x'),
|
|
215
|
+
Object('2026-01-01T00:00:00'),
|
|
216
|
+
Object(5),
|
|
217
|
+
new Proxy({}, { get: boom }),
|
|
218
|
+
new Proxy({}, { getPrototypeOf: boom }),
|
|
219
|
+
Object.create(null),
|
|
220
|
+
() => 0,
|
|
221
|
+
]
|
|
222
|
+
for (let v of hostile) {
|
|
223
|
+
let got
|
|
224
|
+
assert.doesNotThrow(() => {
|
|
225
|
+
got = toFloating(v)
|
|
226
|
+
})
|
|
227
|
+
assert.strictEqual(got, null)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('只接受與條件側相同之形式: 純日期、年0、5位年、帶空白、小寫t一律null; 年1–9999邊界可', function() {
|
|
232
|
+
for (let s of ['2026-01-01', '0000-01-01T00:00:00', '10000-01-01T00:00:00', ' 2026-01-01T00:00:00', '2026-01-01t00:00:00']) {
|
|
233
|
+
assert.strictEqual(toFloating(s), null, s)
|
|
234
|
+
}
|
|
235
|
+
assert.strictEqual(toFloating('0001-01-01T00:00:00').toISOString(), '0001-01-01T00:00:00.000Z')
|
|
236
|
+
assert.strictEqual(toFloating('9999-12-31T23:59:59.999').toISOString(), '9999-12-31T23:59:59.999Z')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('小數秒取前三位(截斷至毫秒, 不進位); 不足三位補零', function() {
|
|
240
|
+
assert.strictEqual(toFloating('2026-01-01T00:00:00.123456').getUTCMilliseconds(), 123)
|
|
241
|
+
assert.strictEqual(toFloating('2026-01-01T00:00:00.9').getUTCMilliseconds(), 900)
|
|
242
|
+
assert.strictEqual(toFloating('2026-01-01T00:00:00.05').getUTCMilliseconds(), 50)
|
|
243
|
+
//截斷與四捨五入不同之值: .1239 → 123(四捨五入為124); .9999 → 999(四捨五入為1000而進位到下一秒)
|
|
244
|
+
assert.strictEqual(toFloating('2026-01-01T00:00:00.1239').getUTCMilliseconds(), 123)
|
|
245
|
+
assert.strictEqual(toFloating('2026-01-01T00:00:00.9999').toISOString(), '2026-01-01T00:00:00.999Z')
|
|
246
|
+
assert.strictEqual(toFloating('2026-12-31T23:59:59.9999').toISOString(), '2026-12-31T23:59:59.999Z')
|
|
247
|
+
assert.strictEqual(toFloating('9999-12-31T23:59:59.9999').toISOString(), '9999-12-31T23:59:59.999Z')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('Date以內建getTime讀內部時間: 跨realm之Date可換算; 自有getTime/getFullYear之覆寫、改寫原型getter皆不影響, 與條件側(cvOrmFind複製Date)認定一致(外審sol F1)', function() {
|
|
251
|
+
for (let tz of ['UTC', 'America/Los_Angeles', 'Asia/Taipei']) {
|
|
252
|
+
withTz(tz, () => {
|
|
253
|
+
let base = toFloating(new Date('2026-01-01T12:00:00Z'))
|
|
254
|
+
let cross = vm.runInNewContext('new Date(\'2026-01-01T12:00:00Z\')')
|
|
255
|
+
assert.ok(!(cross instanceof Date), '前提: 跨realm之Date不是本realm之Date實例')
|
|
256
|
+
assert.strictEqual(toFloating(cross).getTime(), base.getTime(), `${tz} 跨realm`)
|
|
257
|
+
assert.strictEqual(md.where({ d: cross }, { dateNoTz: 'floating' }), md.where({ d: new Date('2026-01-01T12:00:00Z') }, { dateNoTz: 'floating' }), `${tz} 條件側同`)
|
|
258
|
+
//自有getFullYear/getTime/getHours覆寫
|
|
259
|
+
let evil = new Date('2026-01-01T12:00:00Z')
|
|
260
|
+
evil.getFullYear = () => 2099
|
|
261
|
+
evil.getHours = () => 3
|
|
262
|
+
evil.getTime = () => {
|
|
263
|
+
throw new Error('custom getTime')
|
|
264
|
+
}
|
|
265
|
+
assert.strictEqual(toFloating(evil).getTime(), base.getTime(), `${tz} 覆寫`)
|
|
266
|
+
//Date子類別
|
|
267
|
+
class MyDate extends Date {
|
|
268
|
+
getTime() {
|
|
269
|
+
return 0
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
assert.strictEqual(toFloating(new MyDate('2026-01-01T12:00:00Z')).getTime(), base.getTime(), `${tz} 子類別`)
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
//不是Date之物件(含偽裝Symbol.toStringTag、有getTime方法者)為null
|
|
276
|
+
let fake = { getTime: () => 0, getFullYear: () => 2026, [Symbol.toStringTag]: 'Date' }
|
|
277
|
+
assert.strictEqual(toFloating(fake), null)
|
|
278
|
+
assert.strictEqual(toFloating(Object.create(Date.prototype)), null)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('帶時區偏移字串 x 偏移寫法(+05:45、-08:00、+0530、-0800、Z、+00:00) x 分隔(T、空白) x 秒/毫秒有無 x 各時區: 取瞬間於主機時區之牆上分量(預言機Intl); 含負偏移(外審sol F3)', function() {
|
|
282
|
+
let offs = [['+05:45', 345], ['-08:00', -480], ['+0530', 330], ['-0800', -480], ['Z', 0], ['+00:00', 0], ['-03:30', -210]]
|
|
283
|
+
let walls = [[2026, 1, 15, 12, 0, 0, 0], [2026, 3, 8, 2, 30, 0, 0], [2026, 11, 1, 1, 30, 0, 0], [9999, 12, 30, 12, 0, 0, 0], [100, 1, 1, 8, 0, 0, 0]]
|
|
284
|
+
let n = 0
|
|
285
|
+
for (let tz of TZS) {
|
|
286
|
+
withTz(tz, () => {
|
|
287
|
+
for (let [txt, off] of offs) {
|
|
288
|
+
for (let sep of ['T', ' ']) {
|
|
289
|
+
for (let w of walls) {
|
|
290
|
+
for (let form of ['sec', 'nosec', 'ms']) {
|
|
291
|
+
let s = `${pad(w[0], 4)}-${pad(w[1])}-${pad(w[2])}${sep}${pad(w[3])}:${pad(w[4])}${form === 'nosec' ? '' : `:${pad(w[5])}`}${form === 'ms' ? '.250' : ''}${txt}`
|
|
292
|
+
let inst = utcOf([w[0], w[1], w[2], w[3], w[4], form === 'nosec' ? 0 : w[5], form === 'ms' ? 250 : 0]).getTime() - off * 60000
|
|
293
|
+
let wi = wallIntl(inst, tz)
|
|
294
|
+
let want = inYear(wi[0]) ? utcOf(wi) : null
|
|
295
|
+
let got = toFloating(s)
|
|
296
|
+
n += 1
|
|
297
|
+
if (want === null) {
|
|
298
|
+
assert.strictEqual(got, null, `${tz} ${s}`)
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
assert.ok(got !== null && got.getTime() === want.getTime(), `${tz} ${s}: 得 ${got === null ? null : got.toISOString()} 應為 ${want.toISOString()}`)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
assert.strictEqual(n, 10 * 7 * 2 * 5 * 3)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('帶時區字串曆法上不存在者為null(V8之Date.parse會順延日期而接受2026-02-30T00:00:00Z, 故須先驗曆法), 各時區皆然', function() {
|
|
313
|
+
let bads = ['2026-02-30T00:00:00Z', '2025-02-29T00:00:00+08:00', '2026-04-31T00:00:00Z', '2026-02-29 12:00:00Z', '2026-13-01T00:00:00Z', '2026-00-10T00:00:00Z', '2026-01-00T00:00:00Z']
|
|
314
|
+
for (let tz of TZS) {
|
|
315
|
+
withTz(tz, () => {
|
|
316
|
+
for (let s of bads) {
|
|
317
|
+
assert.strictEqual(toFloating(s), null, `${tz} ${s}`)
|
|
318
|
+
throwsMsg(() => cvFind({ d: s }, { ...md.ctx, dateNoTz: 'floating' }), 'does not match column type [date]')
|
|
319
|
+
}
|
|
320
|
+
//閏日與合法者可
|
|
321
|
+
assert.ok(toFloating('2024-02-29T12:00:00Z') instanceof Date)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
describe('cvFind ctx.dateNoTz:\'floating\'', function() {
|
|
330
|
+
|
|
331
|
+
it('值域: undefined、\'local\'、\'floating\'可; 其餘(含大小寫不同、null、false、空字串、數值)於入口拋invalid ctx, 含find為空或非物件時', function() {
|
|
332
|
+
for (let v of [undefined, 'local', 'floating']) {
|
|
333
|
+
assert.doesNotThrow(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...md.ctx, dateNoTz: v }))
|
|
334
|
+
}
|
|
335
|
+
for (let v of [null, false, true, '', 'Floating', 'FLOATING', 'floating ', 'float', 'utc', 'Local', 0, 1, {}, []]) {
|
|
336
|
+
throwsMsg(() => cvFind({ d: '2026-01-01T00:00:00Z' }, { ...md.ctx, dateNoTz: v }), 'invalid ctx: dateNoTz must be \'local\', \'floating\' or undefined')
|
|
337
|
+
throwsMsg(() => cvFind({}, { ...md.ctx, dateNoTz: v }), 'invalid ctx')
|
|
338
|
+
throwsMsg(() => cvFind(undefined, { ...md.ctx, dateNoTz: v }), 'invalid ctx')
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('未給時維持reject無時區字串(預設行為不變)', function() {
|
|
343
|
+
throwsMsg(() => cvFind({ d: '2026-01-01T00:00:00' }, md.ctx), '[date]')
|
|
344
|
+
throwsMsg(() => cvFind({ d: '2026-01-01T00:00:00' }, { ...md.ctx, dateNoTz: undefined }), '[date]')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
for (let tz of TZS.concat(TZS_DST_EAST)) {
|
|
348
|
+
|
|
349
|
+
it(`${tz}: 無時區字串之SQL與主機時區無關(牆上分量即UTC分量), 含9999哨兵、年0100與夏令跳過/重疊之牆上時間`, function() {
|
|
350
|
+
withTz(tz, () => {
|
|
351
|
+
for (let s of ['2026-10-20T00:00:00', '9999-12-31T23:59:59', '0100-01-01T00:00:00', '2026-03-08T02:30:00', '2026-11-01T01:30:00', '2026-03-29T02:30:00', '2026-10-04T02:15:00', '2026-10-20 12:34:56.789']) {
|
|
352
|
+
let w = md.where({ d: { $lt: s } }, { dateNoTz: 'floating' })
|
|
353
|
+
let want = ut(utcOf([Number(s.slice(0, 4)), Number(s.slice(5, 7)), Number(s.slice(8, 10)), Number(s.slice(11, 13)), Number(s.slice(14, 16)), Number(s.slice(17, 19)), s.length > 20 ? Number(s.slice(20, 23)) : 0]))
|
|
354
|
+
assert.ok(w.indexOf(want) >= 0, `${tz} ${s}: ${w} 應含 ${want}`)
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it(`${tz}: 本地分量建構之Date與其epoch恆為同一牆上時間之浮動值(與主機時區無關)`, function() {
|
|
360
|
+
withTz(tz, () => {
|
|
361
|
+
for (let c of [[2026, 0, 1, 0, 0, 0], [2026, 9, 20, 12, 34, 56], [2020, 5, 15, 23, 59, 59], [100, 0, 1, 0, 0, 0]]) {
|
|
362
|
+
let d = new Date(2000, 0, 1)
|
|
363
|
+
d.setFullYear(c[0], c[1], c[2])
|
|
364
|
+
d.setHours(c[3], c[4], c[5], 0)
|
|
365
|
+
let wantD = ut(utcOf([d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), 0]))
|
|
366
|
+
assert.ok(md.where({ d }, { dateNoTz: 'floating' }).indexOf(wantD) >= 0, `${tz} Date ${d.toString()}`)
|
|
367
|
+
assert.ok(md.where({ d: d.getTime() }, { dateNoTz: 'floating' }).indexOf(wantD) >= 0, `${tz} epoch`)
|
|
368
|
+
//該牆上時間若為夏令跳過者(本地建構已被正規化)以結果Date之本地分量為準, 此處以非跳過時刻之案固定; 年0100~2026之案皆非跳過
|
|
369
|
+
if (c[0] >= 1900) {
|
|
370
|
+
assert.strictEqual(wantD.slice(0, 4), pad(c[0], 4))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it(`${tz}: 帶Z字串依瞬間取主機本地分量(預言機Intl)`, function() {
|
|
377
|
+
withTz(tz, () => {
|
|
378
|
+
for (let z of ['2026-10-20T00:00:00Z', '2026-01-15T12:00:00Z', '2026-07-15T12:00:00.5Z', '2026-10-20T00:00:00+05:45']) {
|
|
379
|
+
let inst = Date.parse(z)
|
|
380
|
+
let wi = wallIntl(inst, tz)
|
|
381
|
+
let w = md.where({ d: { $eq: z } }, { dateNoTz: 'floating' })
|
|
382
|
+
assert.ok(w.indexOf(ut(utcOf(wi))) >= 0, `${tz} ${z}: ${w} 應含 ${ut(utcOf(wi))}`)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
it('固定瞬間(Date、epoch、帶Z字串)之浮動值隨主機時區而變(此即語意: 取瞬間於主機時區之牆上分量); 無時區字串則恆定(外審sol F8)', function() {
|
|
390
|
+
let at = (tz, v) => withTz(tz, () => md.where({ d: { $eq: v } }, { dateNoTz: 'floating' }))
|
|
391
|
+
for (let mk of [() => new Date('2026-01-01T00:00:00Z'), () => 1767225600000, () => '2026-01-01T00:00:00Z']) {
|
|
392
|
+
assert.ok(at('UTC', mk()).indexOf('2026-01-01 00:00:00.000 +00:00') >= 0)
|
|
393
|
+
assert.ok(at('Asia/Taipei', mk()).indexOf('2026-01-01 08:00:00.000 +00:00') >= 0)
|
|
394
|
+
assert.ok(at('America/Los_Angeles', mk()).indexOf('2025-12-31 16:00:00.000 +00:00') >= 0)
|
|
395
|
+
}
|
|
396
|
+
for (let tz of ['UTC', 'Asia/Taipei', 'America/Los_Angeles']) {
|
|
397
|
+
assert.ok(at(tz, '2026-01-01T00:00:00').indexOf('2026-01-01 00:00:00.000 +00:00') >= 0)
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('$in/$nin: 每個元素皆換算; 任一元素不合法則整體拒收', function() {
|
|
402
|
+
let w = md.where({ d: { $in: ['2026-01-01T00:00:00', '2026-06-01T12:00:00'] } }, { dateNoTz: 'floating' })
|
|
403
|
+
assert.ok(w.indexOf('2026-01-01 00:00:00.000 +00:00') >= 0 && w.indexOf('2026-06-01 12:00:00.000 +00:00') >= 0, w)
|
|
404
|
+
let w2 = md.where({ d: { $nin: ['2026-01-01T00:00:00', '2026-06-01T12:00:00'] } }, { dateNoTz: 'floating' })
|
|
405
|
+
assert.ok(w2.indexOf('NOT IN') >= 0 && w2.indexOf('2026-06-01 12:00:00.000 +00:00') >= 0, w2)
|
|
406
|
+
throwsMsg(() => cvFind({ d: { $in: ['2026-01-01T00:00:00', '2026-02-30T00:00:00'] } }, { ...md.ctx, dateNoTz: 'floating' }), '[date]')
|
|
407
|
+
throwsMsg(() => cvFind({ d: { $nin: ['2026-01-01T00:00:00', 'garbage'] } }, { ...md.ctx, dateNoTz: 'floating' }), '[date]')
|
|
408
|
+
//空$in仍為恆假(以引用欄位之f IS NULL AND 1 = 0表示)
|
|
409
|
+
let w3 = md.where({ d: { $in: [] } }, { dateNoTz: 'floating' })
|
|
410
|
+
assert.ok(/d.{0,3} IS NULL AND 1 = 0/.test(w3), w3)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('不合法者拒收(訊息同現行): 純日期、曆法不存在、24時、60秒、年0、5位年、布林、字串亂碼', function() {
|
|
414
|
+
for (let s of ['2026-01-01', '2026-02-30T00:00:00', '2025-02-29T00:00:00', '2026-13-01T00:00:00', '2026-01-01T24:00:00', '2026-01-01T00:00:60', '0000-01-01T00:00:00', '10000-01-01T00:00:00', 'garbage', '2026-01-01T00:00:00+8', ' 2026-01-01T00:00:00']) {
|
|
415
|
+
throwsMsg(() => cvFind({ d: s }, { ...md.ctx, dateNoTz: 'floating' }), 'does not match column type [date]')
|
|
416
|
+
}
|
|
417
|
+
throwsMsg(() => cvFind({ d: true }, { ...md.ctx, dateNoTz: 'floating' }), 'does not match column type [date]')
|
|
418
|
+
//超出Date範圍之epoch與無效Date由形狀層先拒
|
|
419
|
+
throwsMsg(() => cvFind({ d: 8.64e15 + 1 }, { ...md.ctx, dateNoTz: 'floating' }), 'does not match column type [date]')
|
|
420
|
+
throwsMsg(() => cvFind({ d: new Date(NaN) }, { ...md.ctx, dateNoTz: 'floating' }), 'invalid find')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('僅作用於DATE欄: DATEONLY、字串、整數欄不受影響(與未給dateNoTz同)', function() {
|
|
424
|
+
let cx = { dateNoTz: 'floating' }
|
|
425
|
+
assert.strictEqual(md.where({ o: '2026-01-01' }, cx), md.where({ o: '2026-01-01' }))
|
|
426
|
+
throwsMsg(() => cvFind({ o: '2026-01-01T00:00:00' }, { ...md.ctx, ...cx }), '[dateonly]')
|
|
427
|
+
throwsMsg(() => cvFind({ o: new Date(0) }, { ...md.ctx, ...cx }), '[dateonly]')
|
|
428
|
+
assert.strictEqual(md.where({ s: '2026-01-01T00:00:00' }, cx), md.where({ s: '2026-01-01T00:00:00' }))
|
|
429
|
+
assert.ok(md.where({ s: '2026-01-01T00:00:00' }, cx).indexOf('\'2026-01-01T00:00:00\'') >= 0)
|
|
430
|
+
assert.strictEqual(md.where({ i: 5 }, cx), md.where({ i: 5 }))
|
|
431
|
+
throwsMsg(() => cvFind({ i: '2026-01-01T00:00:00' }, { ...md.ctx, ...cx }), '[int]')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('null仍為IS NULL; $ne:null為IS NOT NULL(不受浮動換算影響)', function() {
|
|
435
|
+
let cx = { dateNoTz: 'floating' }
|
|
436
|
+
assert.strictEqual(md.where({ d: null }, cx), md.where({ d: null }))
|
|
437
|
+
assert.strictEqual(md.where({ d: { $ne: null } }, cx), md.where({ d: { $ne: null } }))
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('與\'local\'之差異: 負偏移時區之9999哨兵與夏令跳過之牆上時間, local拒收而floating可', function() {
|
|
441
|
+
withTz('America/Los_Angeles', () => {
|
|
442
|
+
throwsMsg(() => cvFind({ d: { $gte: '9999-12-31T23:59:59' } }, { ...md.ctx, dateNoTz: 'local' }), '[date]')
|
|
443
|
+
assert.ok(md.where({ d: { $gte: '9999-12-31T23:59:59' } }, { dateNoTz: 'floating' }).indexOf('9999-12-31 23:59:59.000 +00:00') >= 0)
|
|
444
|
+
})
|
|
445
|
+
withTz('America/New_York', () => {
|
|
446
|
+
throwsMsg(() => cvFind({ d: '2026-03-08T02:30:00' }, { ...md.ctx, dateNoTz: 'local' }), '[date]')
|
|
447
|
+
assert.ok(md.where({ d: '2026-03-08T02:30:00' }, { dateNoTz: 'floating' }).indexOf('2026-03-08 02:30:00.000 +00:00') >= 0)
|
|
448
|
+
//重疊時刻: local取較早者(EDT, +4h), floating依牆上時間(01:30 UTC)
|
|
449
|
+
assert.ok(md.where({ d: '2026-11-01T01:30:00' }, { dateNoTz: 'local' }).indexOf('2026-11-01 05:30:00.000 +00:00') >= 0)
|
|
450
|
+
assert.ok(md.where({ d: '2026-11-01T01:30:00' }, { dateNoTz: 'floating' }).indexOf('2026-11-01 01:30:00.000 +00:00') >= 0)
|
|
451
|
+
})
|
|
452
|
+
withTz('Europe/Berlin', () => {
|
|
453
|
+
throwsMsg(() => cvFind({ d: '2026-03-29T02:30:00' }, { ...md.ctx, dateNoTz: 'local' }), '[date]')
|
|
454
|
+
assert.ok(md.where({ d: '2026-03-29T02:30:00' }, { dateNoTz: 'floating' }).indexOf('2026-03-29 02:30:00.000 +00:00') >= 0)
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('\'local\'行為不變: 正偏移/UTC時區之local與既有一致(同一字串之SQL = 本地分量建構之Date)', function() {
|
|
459
|
+
withTz('Asia/Taipei', () => {
|
|
460
|
+
assert.strictEqual(md.where({ d: '2026-10-20T00:00:00' }, { dateNoTz: 'local' }), md.where({ d: new Date(2026, 9, 20, 0, 0, 0) }))
|
|
461
|
+
assert.ok(md.where({ d: '2026-10-20T00:00:00' }, { dateNoTz: 'local' }).indexOf('2026-10-19 16:00:00.000 +00:00') >= 0)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
//契約①: 編碼後之浮動Date/epoch不可再當'floating'條件輸入(Date一律被當作瞬間取本地分量, 再次輸入即二次位移)
|
|
466
|
+
it('契約①: 編碼後之浮動Date再當條件輸入即二次位移(非UTC時區下toFloating非冪等; UTC下恆等)', function() {
|
|
467
|
+
for (let tz of TZS) {
|
|
468
|
+
withTz(tz, () => {
|
|
469
|
+
let once = toFloating('2026-10-20T12:00:00')
|
|
470
|
+
let twice = toFloating(once)
|
|
471
|
+
let off = once.getTimezoneOffset() //該瞬間之偏移(分鐘, UTC減本地)
|
|
472
|
+
if (off === 0) {
|
|
473
|
+
assert.strictEqual(twice.getTime(), once.getTime(), `${tz}: UTC下應冪等`)
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
assert.notStrictEqual(twice.getTime(), once.getTime(), `${tz}: 非UTC時區下編碼後之Date再編碼應位移`)
|
|
477
|
+
assert.strictEqual(twice.getTime(), once.getTime() - off * 60000, `${tz}: 位移量應為該瞬間之本地偏移`)
|
|
478
|
+
}
|
|
479
|
+
//經cvFind: 原字串與編碼後Date產生之條件值不同(非UTC時區)
|
|
480
|
+
let w1 = md.where({ d: { $eq: '2026-10-20T12:00:00' } }, { dateNoTz: 'floating' })
|
|
481
|
+
let w2 = md.where({ d: { $eq: once } }, { dateNoTz: 'floating' })
|
|
482
|
+
if (off === 0) {
|
|
483
|
+
assert.strictEqual(w1, w2)
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
assert.notStrictEqual(w1, w2, `${tz}: 編碼後之Date當條件輸入應與原條件不同(二次位移)`)
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
})
|