_numOpt.mjs

//本檔為內部使用, 不由index匯出


/**
 * isenum與iseint共用之opt解析(單一擁有者)
 *
 * 規格: 主控dspec〈輸入參數之檢測機制〉K3(非預期之輸入視為未給, 改用預設; opt.debug警告)、§4 V4~V7、V11、§6 V20、§7.1
 *
 * opt為選填之設定, 非預期之輸入一律視為未給、改用預設, 不拋錯(使用者 2026-10-31: 「isenum/iseint 自己的 opt 也是給預設」):
 *   opt為undefined或null視為未給(呼叫端不進入本函數)
 *   opt須為一般物件(原型為Object.prototype或null, 不含陣列、類別實例、包裝物件、Date等), 否則整份視為{}
 *   各鍵為undefined或null視為未給; 給了而不屬該鍵之種類者視為未給(use改用預設)
 *   未宣告之鍵忽略(本函數不轉傳opt, 故於debug時提示, 如誤拼之minExlusive)
 *   每個鍵只讀一次(快照), 讀取拋錯(存取器)視為該鍵無效
 *   範圍為空(含與use之交集為空)時, 明確給定之界值(min、max、gt、lt)皆視為未給, use保留
 *
 * opt.debug(布林, 預設false; 非布林視為false): 為true時, 非預期之輸入另輸出警告, 說明哪個鍵因非預期已改用預設:
 *   警告出口: opt.funLog為函數者交funLog(msg, info), 否則console.warn(msg); 出口拋錯一律忽略
 *   警告為固定詞彙, 只含函數名、鍵名(僅短識別字)、種類描述與所採之預設(程式常數), 不含傳入之值(K9、V20)
 *   同一函數、同一鍵、同一原因每個行程只警告一次(逐筆呼叫之判斷函數不洗版); 去重表有上限, 滿了即不再警告
 *   opt本身非一般物件時整份視為{}, 讀不到debug, 故不警告
 *
 * @param {*} opt 呼叫端傳入之opt, 已確認不為undefined與null
 * @param {Object} spec 宣告表: { fn: 函數名, keys: 已宣告之鍵(Set), table: use範圍表, names: use名稱清單, useDef: 預設use或null, check: 界值檢測函數(v)=>Boolean, desc: 界值種類描述, boundKeys: 界值鍵之字串(訊息用) }
 * @returns {Object} 回傳範圍物件 { min, max, gt, lt }, 未限制者為±Infinity; min、max為含端點, gt、lt為不含端點, 皆為number
 */
function parseNumOpt(opt, spec) {

    let min = -Infinity
    let max = Infinity
    let gt = -Infinity
    let lt = Infinity
    let use = spec.useDef
    let debug = false
    let funLog = null
    let probs = null //非預期之輸入, 僅debug時才輸出: [原因, 鍵名]

    //opt本身須為一般物件, 否則整份視為{}
    let keys = null
    if (isPlainObj(opt)) {
        try {
            keys = Object.keys(opt)
        }
        catch (err) {
            keys = null
        }
    }

    //逐鍵讀取(快照)
    if (keys !== null) {
        for (let k of keys) {

            //未宣告之鍵, 忽略; 鍵名僅在為短識別字時帶入警告(鍵名非值, 但避免回顯任意長字串)
            if (!spec.keys.has(k)) {
                probs = addProb(probs, 'unknown', IDENT.test(k) ? k : '')
                continue
            }

            //讀取一次
            let v
            try {
                v = opt[k]
            }
            catch (err) {
                probs = addProb(probs, 'read', k)
                continue
            }

            //undefined與null視為未給
            if (v === undefined || v === null) {
                continue
            }

            if (k === 'debug') {
                debug = v === true //非布林視為false
                continue
            }
            if (k === 'funLog') {
                funLog = typeof v === 'function' ? v : null
                continue
            }
            if (k === 'use') {
                if (typeof v === 'string' && spec.table[v] !== undefined) {
                    use = v
                }
                else {
                    probs = addProb(probs, 'use', 'use')
                }
                continue
            }

            //min、max、gt、lt, 須為有效界值, 轉換後只用轉換值
            if (!spec.check(v)) {
                probs = addProb(probs, 'bound', k)
                continue
            }
            let n = Number(v)
            if (k === 'min') {
                min = n
            }
            else if (k === 'max') {
                max = n
            }
            else if (k === 'gt') {
                gt = n
            }
            else {
                lt = n
            }

        }
    }

    //與use取交集, 取較嚴者
    let r = use !== null ? spec.table[use] : null
    let rmin = r !== null && r.min !== undefined ? r.min : -Infinity
    let rmax = r !== null && r.max !== undefined ? r.max : Infinity
    let rgt = r !== null && r.gt !== undefined ? r.gt : -Infinity
    let rlt = r !== null && r.lt !== undefined ? r.lt : Infinity
    let emin = Math.max(min, rmin)
    let emax = Math.min(max, rmax)
    let egt = Math.max(gt, rgt)
    let elt = Math.min(lt, rlt)

    //範圍為空(含與use之交集為空), 明確給定之界值皆視為未給, use保留; 下界取min與gt之較大者, 相等時以不含端點為準
    let lo = Math.max(emin, egt)
    let hi = Math.min(emax, elt)
    if (lo > hi || (lo === hi && (egt >= emin || elt <= emax))) {
        probs = addProb(probs, 'empty', '')
        emin = rmin
        emax = rmax
        egt = rgt
        elt = rlt
    }

    //debug時輸出警告
    if (debug && probs !== null) {
        emit(probs, spec, funLog)
    }

    return { min: emin, max: emax, gt: egt, lt: elt }
}


//是否為短識別字(警告可帶入之鍵名)
let IDENT = /^[A-Za-z_$][\w$]{0,31}$/


//累積非預期之輸入
function addProb(probs, reason, key) {
    if (probs === null) {
        probs = []
    }
    probs.push([reason, key])
    return probs
}


//是否為一般物件: 原型為null, 或原型之原型為null且原型自有isPrototypeOf(即任一領域之Object.prototype)
function isPlainObj(o) {
    if (typeof o !== 'object' || o === null) {
        return false
    }
    try {
        if (Object.prototype.toString.call(o) !== '[object Object]') {
            return false
        }
        let p = Object.getPrototypeOf(o)
        if (p === null) {
            return true
        }
        return Object.getPrototypeOf(p) === null && Object.prototype.hasOwnProperty.call(p, 'isPrototypeOf')
    }
    catch (err) {
        return false
    }
}


//已警告之表(每個行程), 有上限避免呼叫端以大量不同鍵名撐大
let WARNED_MAX = 256
let warned = new Set()


//重設已警告之表(供測試)
function resetWarned() {
    warned.clear()
}


//警告訊息(固定詞彙, 不含傳入之值)
function getMsg(reason, key, spec) {
    let s = `[wsemi ${spec.fn}] `
    if (reason === 'unknown') {
        return key !== '' ? `${s}invalid opt.${key}: unknown option, ignored` : `${s}invalid opt: unknown option, ignored`
    }
    if (reason === 'read') {
        return `${s}invalid opt.${key}: cannot be read, ignored`
    }
    if (reason === 'use') {
        let ns = spec.names.map((n) => `'${n}'`).join(', ')
        return `${s}invalid opt.use: must be one of ${ns}, ${spec.useDef !== null ? `using default '${spec.useDef}'` : 'ignored'}`
    }
    if (reason === 'bound') {
        return `${s}invalid opt.${key}: must be ${spec.desc}, ignored`
    }
    return `${s}invalid opt: range is empty, ${spec.boundKeys} ignored` //empty
}


//輸出一則警告(單一出口, 供本檔與_optVal.mjs共用): 同一id每個行程只輸出一次, 去重表滿了即不再輸出; 有funLog者交funLog(msg, info), 否則console.warn(msg); 出口拋錯一律忽略
function warnOnce(id, msg, info, funLog) {
    if (warned.has(id)) {
        return
    }
    if (warned.size >= WARNED_MAX) {
        return
    }
    warned.add(id)
    try {
        if (typeof funLog === 'function') {
            let r = funLog(msg, info)

            //出口回傳thenable(如async函數)時, 其rejection一律忽略, 否則成為unhandledRejection而使行程崩潰
            if (r !== null && r !== undefined && typeof r.then === 'function') {
                r.then(undefined, () => {})
            }
        }
        else {
            console.warn(msg)
        }
    }
    catch (err) {
        //忽略
    }
}


//輸出警告: 有funLog者交funLog(msg, info), 否則console.warn(msg); 出口拋錯一律忽略
function emit(probs, spec, funLog) {
    for (let [reason, key] of probs) {
        warnOnce(`${spec.fn}|${reason}|${key}`, getMsg(reason, key, spec), { fn: spec.fn, reason, key }, funLog)
    }
}


parseNumOpt.resetWarned = resetWarned
parseNumOpt.warnOnce = warnOnce


export default parseNumOpt