コピペで済ませる。JavaScript 環境ならどこでも使えそうなそんなに長くないコードで、フォーマット指定して文字列からDate型に変換する関数

# 前がき

タイトル長くてすいません。

前回、こんな記事を書きました。

https://qiita.com/standard-software/items/fe8dfcbeb3bde37bd2f4

その記事は、dateToString なので、日付を文字列に変換です。

今回の記事は stringToDate です。文字列から日付変換です。

単に日付を書式付きで出力するのは前回記事のようにまずまず簡単なのですが、出力した文字列を日付型に戻すのは結構たいへんなので、そこで自前実装をあきらめて Moment.js や Day.js を導入するきっかけにすることは非常に多くあるのではないでしょうか。

とはいえ、日付ライブラリをいれたくないなあ、という場面もあるかもしれなく、また、私は自前実装をあきらめないタイプなので実装してみました。

日付を文字列としてデータベースやlocalStorageやテキストファイルとかに記録しておいて、その文字列を日付に戻して動作させるという処理は便利なので多くありますので、そういう時に使えるでしょう。

少し長いのですがコピペコードは次の通り。コードが長いのは、前回の dateToString のマルチフォーマット版(前回記事のコメント欄で作ってる)を組み込んでいるからです。

# コード

```js
const indexOfAnyFirst = (
  str, searchArray, indexStart = 0,
) => {
  let result = Infinity;
  let searchIndex = -1;
  searchArray.forEach((search, index) => {
    const findIndex = str.indexOf(search, indexStart);
    if (findIndex !== -1) {
      if (findIndex < result) {
        result = findIndex;
        searchIndex = index;
      }
    }
  });

  if (result === Infinity) {
    return {
      index: -1,
      searchIndex: -1,
    };
  }
  return {
    index: result,
    searchIndex,
  };
};

const replaceAllArray = (str, replaceArray) => {
  const searchArray = replaceArray.map(element => element[0]);
  let start = 0;
  let result = '';
  while (true) {
    const searchResult = indexOfAnyFirst(str, searchArray, start);
    if (searchResult.index === -1) {
      result += str.substring(start);
      break;
    }

    if (start < searchResult.index) {
      result += str.substring(start, searchResult.index);
      start = searchResult.index;
    }
    result += replaceArray[searchResult.searchIndex][1];
    start += searchArray[searchResult.searchIndex].length;
  }
  return result;
};

const dateToString = (format, date) => {
  const padFirstZero = (value) => {
    return ('0' + value).slice(-2);
  }
  const year4     = date.getFullYear();
  const year2     = date.getFullYear().toString().slice(-2);
  const month1    = (date.getMonth() + 1).toString();
  const month2    = padFirstZero(month1);
  const date1     = date.getDate().toString();
  const date2     = padFirstZero(date1);
  const hours1    = date.getHours().toString();
  const hours2    = padFirstZero(hours1);
  const minutes1  = date.getMinutes().toString();
  const minutes2  = padFirstZero(minutes1);
  const seconds1  = date.getSeconds().toString();
  const seconds2  = padFirstZero(seconds1);
  const day3      = ['Sun', 'Mon', 'Tue', 'Wed', 'Thr', 'Fri', 'Sat'][date.getDay()]

  const replaceTable = [
    ['YYYY'   , year4],
    ['YY'     , year2],
    ['M'      , month1],
    ['MM'     , month2],
    ['D'      , date1],
    ['DD'     , date2],
    ['H'      , hours1],
    ['HH'     , hours2],
    ['m'      , minutes1],
    ['mm'     , minutes2],
    ['S'      , seconds1],
    ['SS'     , seconds2],
    ['DDD'    , day3],
  ]
  replaceTable.sort((a, b) => b[0].length - a[0].length);
  let result = format;
  return replaceAllArray(result, replaceTable);
};
// const day1 = new Date('2021-04-26')
// console.log(dateToString('YYYY-MM-DD', day1));
// console.log(dateToString('D-M-YY', day1));
// console.log(dateToString('YYYY/MM/DD HH:mm:SS(DDD)', day1));
// console.log(dateToString('YYYYMMDDHHmmSSDDD', day1));
// console.log(dateToString('DDDSSmmHHDDMMYYYY', day1));

// 2021-04-26
// 26-4-21
// 2021/04/26 09:00:00(Mon)
// 20210426090000Mon
// Mon00000926042021


const stringToDate = (format, str) => {

  const YYYY = '(\\d{4})'
  const YY = '(\\d{2})'
  const MM = '(\\d{2})'
  const M = '(\\d{1,2})'
  const DD = '(\\d{2})'
  const D = '(\\d{1,2})'
  const HH = '(\\d{2})'
  const H = '(\\d{1,2})'
  const mm = '(\\d{2})'
  const m = '(\\d{1,2})'
  const SS = '(\\d{2})'
  const S = '(\\d{1,2})'

  const now = new Date();
  const result = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  let resultArray;
  switch (format) {
    case 'YYYY/MM/DD':
      resultArray = str.replace(
        new RegExp(`^${YYYY}/${MM}/${DD}$`),
        '$1,$2,$3'
      ).split(',')
      result.setFullYear(parseInt(resultArray[0]));
      result.setMonth(parseInt(resultArray[1]) - 1);
      result.setDate(parseInt(resultArray[2]));
      break;
    case 'D-M-YY':
      resultArray = str.replace(
        new RegExp(`^${D}-${M}-${YY}$`),
        '$1,$2,$3'
      ).split(',')
      result.setFullYear(parseInt(resultArray[2]) + 2000);
      result.setMonth(parseInt(resultArray[1]) - 1);
      result.setDate(parseInt(resultArray[0]));
      break;
    case 'M-D-YY':
      resultArray = str.replace(
        new RegExp(`^${M}-${D}-${YY}$`),
        '$1,$2,$3'
      ).split(',')
      result.setFullYear(parseInt(resultArray[2]) + 2000);
      result.setMonth(parseInt(resultArray[0]) - 1);
      result.setDate(parseInt(resultArray[1]));
      break;
    case 'YYYY/MM/DD HH:mm:SS(DDD)':
      resultArray = str.replace(
        new RegExp(`^${YYYY}/${MM}/${DD} ${HH}:${mm}:${SS}.*$`),
        '$1,$2,$3,$4,$5,$6'
      ).split(',')
      result.setFullYear(parseInt(resultArray[0]));
      result.setMonth(parseInt(resultArray[1]) - 1);
      result.setDate(parseInt(resultArray[2]));
      result.setHours(parseInt(resultArray[3]));
      result.setMinutes(parseInt(resultArray[4]));
      result.setSeconds(parseInt(resultArray[5]));
      break;
    default:
      throw new Error(`stringToDate args:format(=${format}) is not supported.`);
  }

  if (dateToString(format, result) === str) {
    return result;
  } else {
    return new Date('');  // Invalid Date
  }
}

console.log(stringToDate('YYYY/MM/DD', '2021/05/01').toString()); // Sat May 01 2021 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('YYYY/MM/DD', '2021/05/00').toString()); // Invalid Date
console.log(stringToDate('YYYY/MM/DD', '2021/04/30').toString()); // Fri Apr 30 2021 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('YYYY/MM/DD', '2021/04/31').toString()); // Invalid Date

console.log(stringToDate('YYYY/MM/DD', '2021/5/01').toString()); // Invalid Date

console.log(stringToDate('D-M-YY', '1-12-20').toString()); // Tue Dec 01 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('M-D-YY', '12-1-20').toString()); // Tue Dec 01 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('D-M-YY', '21-1-20').toString()); // Tue Jan 21 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('M-D-YY', '1-21-20').toString()); // Tue Jan 21 2020 00:00:00 GMT+0900 (日本標準時)

console.log(stringToDate('M-D-YY', '1-12-20').toString()); // Sun Jan 12 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('M-D-YY', '21-1-20').toString()); // Invalid Date

console.log(
  stringToDate('YYYY/MM/DD HH:mm:SS(DDD)',
  '2021/05/01 11:09:09(Sat)'
).toString());  // Sat May 01 2021 11:09:09 GMT+0900 (日本標準時)

console.log(
  stringToDate('YYYY/MM/DD HH:mm:SS(DDD)',
  '2021/05/01 11:09:09(Mon)'
).toString());  // Invalid Date
```

# コード説明

stringToDate で、日付に変換できない文字列が与えられた場合は、Invalid Date を返すようにしています。(改良してnullやundefinedを返すようにしてもいいと思います。関数の最後の部分を書き換えるだけです。

stringToDate の内部では、dateToString で検算しているので、指定文字列の曜日が変わっていたり、桁数ミスしているだけで、Invalid Date を返します。日付文字列なので厳密判定のほうが安全性が高いのでそのようにしています。

dateToString は、どんなフォーマットでもコード修正なしに動きますが、stringToDate の方は内部でフォーマットを限定しているのであらたなフォーマット指定したければ自分でコード修正して改良してください。

改良しやすく作っているので、フォーマット増やすのは難しくないでしょう。

改良時に気をつけなければいけないのは、setFullYear setMonth など、年月日の順番で設定しなければいけないことです。

初期値を「今日」に指定してから、年月日を設定していっているのですが、「今日」が2月とかだったとして、日月年の順番で設定すると、日の設定で2月31日と設定されたときに、3月3日に変換されたあとに月の設定が動くので日付が正しくあわなくなるからです。

マルチなフォーマット対応のものも作っていきたいところですが、なかなか難しそうですね。

今回はこんなもので。

ではでは。

# 追記 任意フォーマット対応版

コメント欄で @akebi_m さんからかなり短いコードで任意フォーマット対応版を書いていただきました。ご参照ください。str.replaceの正規表現での動作を非常に使いこなしたコードでとても勉強にさせていただいています。

触発されて正規表現の一致を学びなおして別実装になりますが、自分でも実装できたのでこちらに載せておきます。上記と共通関数が重複しているのでえらい長いコードになっていてもはや全くコピペ向きではない気がしますが、まあしょうがない。

ご参考などにどうぞです。

```js
const subIndex = (
  str, indexStart, indexEnd = indexStart,
) => {
  return str.substring(indexStart, indexEnd + 1);
};

const indexOfAnyFirst = (
  str, searchArray, indexStart = 0,
) => {
  let result = Infinity;
  let searchIndex = -1;
  searchArray.forEach((search, index) => {
    const findIndex = str.indexOf(search, indexStart);
    if (findIndex !== -1) {
      if (findIndex < result) {
        result = findIndex;
        searchIndex = index;
      }
    }
  });

  if (result === Infinity) {
    return {
      index: -1,
      searchIndex: -1,
    };
  }
  return {
    index: result,
    searchIndex,
  };
};

const replaceAllArray = (str, replaceArray) => {
  const searchArray = replaceArray.map(element => element[0]);
  let start = 0;
  let result = '';
  while (true) {
    const searchResult = indexOfAnyFirst(str, searchArray, start);
    if (searchResult.index === -1) {
      result += str.substring(start);
      break;
    }

    if (start < searchResult.index) {
      result += str.substring(start, searchResult.index);
      start = searchResult.index;
    }
    result += replaceArray[searchResult.searchIndex][1];
    start += searchArray[searchResult.searchIndex].length;
  }
  return result;
};

const dateToString = (format, date) => {
  const padFirstZero = (value) => {
    return ('0' + value).slice(-2);
  }
  const year4     = date.getFullYear();
  const year2     = date.getFullYear().toString().slice(-2);
  const month1    = (date.getMonth() + 1).toString();
  const month2    = padFirstZero(month1);
  const date1     = date.getDate().toString();
  const date2     = padFirstZero(date1);
  const hours1    = date.getHours().toString();
  const hours2    = padFirstZero(hours1);
  const minutes1  = date.getMinutes().toString();
  const minutes2  = padFirstZero(minutes1);
  const seconds1  = date.getSeconds().toString();
  const seconds2  = padFirstZero(seconds1);
  const day3      = ['Sun', 'Mon', 'Tue', 'Wed', 'Thr', 'Fri', 'Sat'][date.getDay()]

  const replaceTable = [
    ['YYYY'   , year4],
    ['YY'     , year2],
    ['M'      , month1],
    ['MM'     , month2],
    ['D'      , date1],
    ['DD'     , date2],
    ['H'      , hours1],
    ['HH'     , hours2],
    ['m'      , minutes1],
    ['mm'     , minutes2],
    ['S'      , seconds1],
    ['SS'     , seconds2],
    ['DDD'    , day3],
  ]
  replaceTable.sort((a, b) => b[0].length - a[0].length);
  let result = format;
  return replaceAllArray(result, replaceTable);
};
// const day1 = new Date('2021-04-26')
// console.log(dateToString('YYYY-MM-DD', day1));
// console.log(dateToString('D-M-YY', day1));
// console.log(dateToString('YYYY/MM/DD HH:mm:SS(DDD)', day1));
// console.log(dateToString('YYYYMMDDHHmmSSDDD', day1));
// console.log(dateToString('DDDSSmmHHDDMMYYYY', day1));

// 2021-04-26
// 26-4-21
// 2021/04/26 09:00:00(Mon)
// 20210426090000Mon
// Mon00000926042021

const replaceAllArrayDetail = function(str, replaceArray) {
  const searchArray = replaceArray.map(element => element[0]);
  let start = 0;
  let result = '';
  const replaceInfo = [];
  while (true) {
    const searchResult = indexOfAnyFirst(str, searchArray, start);
    if (searchResult.index === -1) {
      result += str.substring(start);
      break;
    }

    if (start < searchResult.index) {
      result += subIndex(str, start, searchResult.index - 1);
      start = searchResult.index;
    }
    result += replaceArray[searchResult.searchIndex][1];
    replaceInfo.push({
      index: searchResult.index,
      searchIndex: searchResult.searchIndex,
    });
    start += searchArray[searchResult.searchIndex].length;
  }
  return {
    result,
    replaceInfo,
  };
};

const stringToDate = (format, str) => {

  const setYear4    = (date, value) => date.setFullYear(parseInt(value));
  const setYear2    = (date, value) => {
    const plusValue = Math.floor((new Date()).getFullYear() / 100) * 100;
    date.setFullYear(parseInt(value) + plusValue)
  };
  const setMonth    = (date, value) => date.setMonth(parseInt(value) - 1);
  const setDate     = (date, value) => date.setDate(parseInt(value));
  const setHours    = (date, value) => date.setHours(parseInt(value));
  const setMinutes  = (date, value) => date.setMinutes(parseInt(value));
  const setSeconds  = (date, value) => date.setSeconds(parseInt(value));

  const replaceTable = [
    ['YYYY',  '(\\d{4})',    1, setYear4],
    ['YY',    '(\\d{2})',    1, setYear2],
    ['MM',    '(\\d{2})',    2, setMonth],
    ['M',     '(\\d{1,2})',  2, setMonth],
    ['DD',    '(\\d{2})',    3, setDate],
    ['D',     '(\\d{1,2})',  3, setDate],
    ['HH',    '(\\d{2})',    4, setHours],
    ['H',     '(\\d{1,2})',  4, setHours],
    ['mm',    '(\\d{2})',    5, setMinutes],
    ['m',     '(\\d{1,2})',  5, setMinutes],
    ['SS',    '(\\d{2})',    6, setSeconds],
    ['S',     '(\\d{1,2})',  6, setSeconds],
    ['DDD',   '',   -1, () => {}],
  ];

  const replaceTextSortFunc = (a, b) => b[0].length - a[0].length;
  const setDatePrioritySortFunc = (a, b) => a[2] - b[2];

  const INVALID_DATE = new Date('');
  let replaceInfoItems = [...replaceTable];
  replaceInfoItems.sort(replaceTextSortFunc);
  const regFormat = replaceAllArrayDetail(
    format,
    replaceInfoItems.map(([text, reg]) => [text, reg])
  );
  replaceInfoItems = regFormat.replaceInfo.map(e => replaceInfoItems[e.searchIndex]);
  // console.log({ regFormat, replaceInfoItems })

  const matchResult = str.match(new RegExp(`${regFormat.result}`));
  if (!Array.isArray(matchResult)) {
    return INVALID_DATE;
  }
  const [match, ...valueItems] = matchResult;
  // console.log({ replaceInfoItems });

  if (replaceInfoItems.length !== valueItems.length) {
    return INVALID_DATE;
  }
  replaceInfoItems = replaceInfoItems.map((e,i) => {
    e.push(valueItems[i]);
    return e;
  });
  replaceInfoItems.sort(setDatePrioritySortFunc);
  // console.log({ replaceInfoItems });

  const now = new Date();
  const result = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
  for (const replaceInfoItem of replaceInfoItems) {
    const setDateFunc = replaceInfoItem[3];
    const setValue = replaceInfoItem[4];
    setDateFunc(result, setValue);
  }

  // console.log(dateToString(format, result), { format, str });
  if (dateToString(format, result) === str) {
    return result;
  } else {
    return INVALID_DATE;
  }
}

console.log(stringToDate('YYYY/MM/DD', '2021/05/01').toString()); // Sat May 01 2021 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('YYYY/MM/DD', '2021/05/00').toString()); // Invalid Date
console.log(stringToDate('YYYY/MM/DD', '2021/04/30').toString()); // Fri Apr 30 2021 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('YYYY/MM/DD', '2021/04/31').toString()); // Invalid Date

console.log(stringToDate('YYYY/MM/DD', '2021/5/01').toString()); // Invalid Date

console.log(stringToDate('D-M-YY', '1-12-20').toString()); // Tue Dec 01 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('M-D-YY', '12-1-20').toString()); // Tue Dec 01 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('D-M-YY', '21-1-20').toString()); // Tue Jan 21 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('M-D-YY', '1-21-20').toString()); // Tue Jan 21 2020 00:00:00 GMT+0900 (日本標準時)

console.log(stringToDate('M-D-YY', '1-12-20').toString()); // Sun Jan 12 2020 00:00:00 GMT+0900 (日本標準時)
console.log(stringToDate('M-D-YY', '21-1-20').toString()); // Invalid Date

console.log(
  stringToDate('YYYY/MM/DD HH:mm:SS',
  '2021/05/01 11:09:09'
).toString());  // Sat May 01 2021 11:09:09 GMT+0900 (日本標準時)
console.log(
  stringToDate('YYYY/MM/DD HH:mm:SS(DDD)',
  '2021/05/01 11:09:09(Sat)'
).toString());  // Sat May 01 2021 11:09:09 GMT+0900 (日本標準時)

console.log(
  stringToDate('YYYY/MM/DD HH:mm:SS(DDD)',
  '2021/05/01 11:09:09(Mon)'
).toString());  // Invalid Date

```