PHPのdate()やmktime()をjavascriptでも使いたい

同様の処理を行うライブラリとしてはdayjsやMoment.jsがメジャーかと思いますが、個人的にはあれほど多機能なものは要らなかったので作成してみました。

`toLocaleDateString()`や`toLocaleTimeString()`等でも色々な表現はできますが、個人的にはやっぱり使い慣れたフォーマット文字列での指定が楽です。

基本的にPHPの`date()`、`mktime()`とほぼ同様に扱えるかと思いますが、以下の既知の相違があります。

####mktime()####
第7引数 **is_dst** はありません。

####date()####
フォーマット文字はPHPで使えるものすべてに対応しているわけではなく、現状 **e** タイムゾーン識別子、**T** タイムゾーンの略称、**I** サマータイム が未対応です。

###参考###
[PHP mktime マニュアル](https://www.php.net/manual/ja/function.mktime.php)
[PHP date マニュアル](https://www.php.net/manual/ja/function.date.php)
[PHP DateTime::format]
(https://www.php.net/manual/ja/datetime.format.php)

###スクリプト###
~~~js:mktime_date.js
/**
 *  日付をUNIXタイムスタンプに変換
 */
function mktime(...args) {

    const
        d = new Date(),
        // 引数受け取り
        hour   = args[0] ?? d.getHours(),
        minute = args[1] ?? d.getMinutes(),
        second = args[2] ?? d.getSeconds(),
        month  = args[3] ?? d.getMonth() + 1,
        day    = args[4] ?? d.getDate(),
        year   = args[5] ?? d.getFullYear(),

        // 年指定補正
        _year = year < 0 || year > 100 ? year :
                year < 70 ? 2000 + year : 1900 + year;

    try {
        // 引数に数値でないものが含まれる
        if( isNaN(hour)   ||
            isNaN(minute) ||
            isNaN(second) ||
            isNaN(month)  ||
            isNaN(day)    ||
            isNaN(year)
        ) throw new Error('A non-numeric value was specified for the argument');
    } catch(e) {
        console.error(e.message);
    }

    // 日時設定
    d.setFullYear(_year, month -1, day);
    d.setHours(hour, minute, second);

    // 戻り値
    return Math.floor(d / 1000);
}

/**
 *  日付/時刻を書式化
 */
function date(format, timeStamp) {

    let milliSeconds = microSeconds = 0;
    // timeStamp未指定の場合は現在のタイムスタンプを設定
    if(timeStamp === undefined) {
        const now =
            performance.timeOrigin ? 
            performance.timeOrigin + performance.now() : Date.now();
        timeStamp    = Math.floor(now / 1000);
        milliSeconds = Math.floor(now) % 1000;
        microSeconds = Math.floor(now * 1000) % 1e6;
    }

    try {
        // format未指定
        if(format === undefined)
            throw new Error('Argument `format` not specified');

        // timeStampが数値ではない
        if(isNaN(timeStamp))
            throw new Error('A non-numeric value was specified for the argument');
    } catch(e) {
        console.error(e.message);
    }

    const
        tzDiff = -Math.floor(new Date(timeStamp * 1000).getTimezoneOffset() * 60),
        absTzDiff = Math.abs(tzDiff),
        d      = new Date(timeStamp * 1000),
        df     = new Date(timeStamp * 1000),

        year   = d.getFullYear(),
        month  = d.getMonth() + 1,
        day    = d.getDate(),
        hour   = d.getHours(),
        minute = d.getMinutes(),
        second = d.getSeconds(),
        w      = d.getDay(),
        spellingMonth =
                 d.toLocaleDateString('en', {month: 'long'}),
        spellingWeek  =
                 d.toLocaleDateString('en', {weekday: 'long'}),

        utcLocalAdj =
                 new Date(+d - d.getTimezoneOffset() * 6e4),
        sDate  = new Date(Math.floor((+utcLocalAdj + 2592e5) / 6048e5) * 6048e5),
        sYear  = sDate.getUTCFullYear(),

        mTimeStamp =
            (timeStamp < 0 ? 86400 - Math.abs(timeStamp) % 86400 : timeStamp) * 1000
                + milliSeconds;

    // 1月1日からの日数
    df.setFullYear(year, 0, 1);
    const days = (d - df) / 864e5;

    // 当月の日数
    df.setFullYear(year, month, 0);
    const daysInMonth = df.getDate();

    // 閏年か
    df.setFullYear(year, 1, 29);
    const leap = df.getDate() === 29 ? 1 : 0;

    // 戻り値
    // 不要なreplaceメソッドチェーンがあれば
    // 適宜コメントアウトや削除をしてください
    return String(format)
        // 文字誤置換防止 下処理
        .replace(/(\w)/g, ":$1:")
        // フォーマット文字エスケープ対応
        .replace(/\\:(\w):/g, "#$1#")

        // RFC2822フォーマットされた日付
        .replace(/:r:/g, ':D:, :d: :M: :Y: :H:::i:::s: :O:')
        // ISO8601 日付
        .replace(/:c:/g, ':Y:-:m:-:d:#T#:H:::i:::s::P:')
        // 年 4桁
        .replace(/:Y:/g, (year < 0 ? '-' : '') +
            String(Math.abs(year)).padStart(4, '0'))
        // 月 2桁
        .replace(/:m:/g, String(month).padStart(2, '0'))
        // 日 2桁
        .replace(/:d:/g, String(day).padStart(2, '0'))
        // 時 24時間単位 2桁
        .replace(/:H:/g, String(hour).padStart(2, '0'))
        // 分 2桁
        .replace(/:i:/g, String(minute).padStart(2, '0'))
        // 秒 2桁
        .replace(/:s:/g, String(second).padStart(2, '0'))

        // 年 2桁
        .replace(/:y:/g, ('0' + year).slice(-2))
        // 月 桁揃えなし
        .replace(/:n:/g, month)
        // 日 桁揃えなし
        .replace(/:j:/g, day)
        // 時 24時間単位 桁揃えなし
        .replace(/:G:/g, hour)
        // 時 12時間単位 2桁
        .replace(/:h:/g, String((hour + 11) % 12 + 1).padStart(2, '0'))
        // 時 12時間単位 桁揃えなし
        .replace(/:g:/g, (hour + 11) % 12 + 1)
        // 閏年か 0:閏年ではない 1:閏年
        .replace(/:L:/g, leap)
        // 日に対する英語形式序数サフィックス
        .replace(/:S:/g, ['th', 'st', 'nd', 'rd']
            [Math.floor(day / 10) % 10 !== 1 && day % 10 < 4 ? day % 10 : 0])

        // 曜日 0:日～6:土
        .replace(/:w:/g, w)
        // ISO8601 曜日 1:月～7:日
        .replace(/:N:/g, (w + 6) % 7 + 1)
        // 年間通算日 0～365
        .replace(/:z:/g, days)
        // 月の日数
        .replace(/:t:/g, daysInMonth)
        // ISO8601 週番号による年
        .replace(/:o:/g, sYear)
        // ISO8601 月曜日に始まる年単位の週番号
        .replace(/:W:/g,
            String(1 + Math.floor((sDate - new Date(sYear + '-01-01T00:00Z')) / 6048e5)).padStart(2, '0'))
        // UNIXタイムスタンプ
        .replace(/:U:/g, timeStamp)
        // ミリ秒
        .replace(/:v:/g, String(milliSeconds).padStart(3, '0'))
        // マイクロ秒
        .replace(/:u:/g, String(microSeconds).padStart(6, '0'))
        // GMTとの時差 ±HH:MM
        .replace(/:P:/g,
            (tzDiff < 0 ? '-' : '+') +
            String(Math.floor(absTzDiff / 3600) % 24).padStart(2, '0') + ':' +
            String(Math.floor(absTzDiff / 60) % 60).padStart(2, '0'))
        // GMTとの時差 ±HHMM
        .replace(/:O:/g,
            (tzDiff < 0 ? '-' : '+') +
            String(Math.floor(absTzDiff / 3600) % 24).padStart(2, '0') +
            String(Math.floor(absTzDiff / 60) % 60).padStart(2, '0'))
        // タイムゾーンオフセット秒数
        .replace(/:Z:/g, tzDiff)

        // Swatchインターネットタイム
        .replace(/:B:/g,
            String(Math.floor(((mTimeStamp + 36e5) % 864e5) / 86400)).padStart(3, '0'))

        // 曜日 3文字
        .replace(/:D:/g, spellingWeek.slice(0, 3))
        // 月 3文字
        .replace(/:M:/g, spellingMonth.slice(0, 3))
        // 曜日 フルスペル
        .replace(/:l:/g, spellingWeek)
        // 月 フルスペル
        .replace(/:F:/g, spellingMonth)
        // am/pm
        .replace(/:a:/g, hour < 12 ? 'am' : 'pm')
        // AM/PM
        .replace(/:A:/g, hour < 12 ? 'AM' : 'PM')
        // 誤置換防止処理 後始末
        .replace(/:(\w):/g , "$1")
        .replace(/#(\w)#/g , "$1")
}

/**
 *  mktimeへ年月日時分秒の順に引数を渡すラッパー
 */
function dateToTime(...args) {
    return mktime(
        args[3], args[4], args[5],
        args[1], args[2], args[0]);
}
~~~

####実行サンプル####

~~~js
// example

// Date.now()でタイムスタンプ取得
time = Math.floor(Date.now() / 1000);
console.log( time ); // 1596325709
// mktime()へ引数なしでのタイムスタンプ取得
time = mktime();
console.log( time ); // 1596325709

formatStr = 'Y-m-d H:i:s y w z t D M l F a A G h g L jS';
console.log( date(formatStr, time) ); // 2020-08-02 08:48:29 20 0 214 31 Sun Aug Sunday August am AM 8 08 8 1 2nd
console.log( date(formatStr) ); // 2020-08-02 08:48:29 20 0 214 31 Sun Aug Sunday August am AM 8 08 8 1 2nd

console.log( date('r', time) ); // Sun, 02 Aug 2020 08:48:29 +0900
console.log( date('o-\\WW-N', time) ); // 2020-W31-7

for(let t=dateToTime(2020,12,29); t<=dateToTime(2021,1,5); t+=86400)
    console.log( date('Y-m-d o-\\WW-N', t) ); 
// 2020-12-29 2020-W53-2
// 2020-12-30 2020-W53-3
// 2020-12-31 2020-W53-4
// 2021-01-01 2020-W53-5
// 2021-01-02 2020-W53-6
// 2021-01-03 2020-W53-7
// 2021-01-04 2021-W01-1
// 2021-01-05 2021-W01-2

console.log( date('c', mktime(0,0,0, 8,1,1800)) ); // 1800-08-01T00:00:00+09:18
console.log( date('c', mktime(0,0,0, 8,1,1950)) ); // 1950-08-01T00:00:00+10:00
console.log( date('c', mktime(0,0,0, 8,1,2020)) ); // 2020-08-01T00:00:00+09:00

console.log( date('Y-m-d\\TH:i:s.vP Z B s') ); // 2020-08-02T08:48:29.853+09:00 32400 033 29

console.log( date('Y-m-d H:i:s D', time) ); // 2020-08-02 08:48:29 Sun

time = mktime(0, 0, 0, 1, 1, 2000);
console.log( date('Y-m-d H:i:s D', time) ); // 2000-01-01 00:00:00 Sat

time = mktime(4, 5, 6, 12, 3, 2000);
console.log( date('Y-m-d H:i:s D', time) ); // 2000-12-03 04:05:06 Sun

time = mktime(0, 0, 0, 2, 29, 2020);
console.log( date('Y-m-d H:i:s D', time) ); // 2020-02-29 00:00:00 Sat

time = mktime(0, 0, 0, 2, 29, 2019);
console.log( date('Y-m-d H:i:s D', time) ); // 2019-03-01 00:00:00 Fri

time = mktime(0, 0, 0, 1, 0, 2000);
console.log( date('Y-m-d H:i:s D', time) ); // 1999-12-31 00:00:00 Fri

time = mktime(0, 0, -1, 1, 1, 2000);
console.log( date('Y-m-d H:i:s D', time) ); // 1999-12-31 23:59:59 Fri

time = mktime(24, 0, 0, 1, 1, 2000);
console.log( date('Y-m-d H:i:s D', time) ); // 2000-01-02 00:00:00 Sun

console.log( date('Y-m-d H:i:s D', 0) ); // 1970-01-01 09:00:00 Thu

console.log(date('Y-m-d H:i:s', mktime(123, 456, 789, -123, -4567)) ); // 1997-03-05 10:49:09

// dateToTime()はmktime()へ年,月,日,時,分,秒の順で引数を渡すラッパーです
console.log(date('Y-m-d H:i:s', dateToTime(2020, 1, 2, 12, 34, 56))); // 2020-01-02 12:34:56
~~~