1月1日~1月5日のような日付範囲をフォーマットする方法

JavaScriptを使用して、ロケールに適した形式と冗長性の削除により日付範囲を表示する

はじめに

日付範囲はWebアプリケーション全体に表示されます。予約システムは1月1日から1月5日までの空き状況を表示し、イベントカレンダーは複数日にわたる会議を表示し、分析ダッシュボードは特定の期間のデータを表示し、レポートは会計四半期またはカスタム日付範囲をカバーします。これらの範囲は、2つのエンドポイント間のすべての日付に何かが適用されることを伝えます。

2つのフォーマット済み日付をダッシュで連結して日付範囲を手動でフォーマットすると、不必要に冗長な出力が作成されます。2024年1月1日から2024年1月5日までの範囲では、2番目の日付で月と年を繰り返す必要はありません。「2024年1月1日~5日」という出力は、冗長な要素を省略することで、同じ情報をより簡潔に伝えます。

JavaScriptはIntl.DateTimeFormatformatRange()メソッドを提供し、日付範囲のフォーマットを自動的に処理します。このメソッドは、範囲の各部分に含める日付要素を決定し、不要な繰り返しを削除しながら、区切り文字とフォーマットにロケール固有の規則を適用します。

日付範囲にインテリジェントなフォーマットが必要な理由

異なる日付範囲には異なるレベルの詳細が必要です。同じ月内の範囲は、複数年にまたがる範囲よりも少ない情報で済みます。最適なフォーマットは、開始日と終了日の間でどの日付要素が異なるかによって決まります。

同じ日内の範囲の場合、時間の差のみを表示する必要があります:「午前10:00~午後2:00」。両方の時刻に完全な日付を繰り返しても、情報は追加されません。

同じ月内の範囲の場合、月を一度だけ表示し、両方の日付を列挙します:「2024年1月1日~5日」。「1月」を2回含めると、明確さを追加することなく出力が読みにくくなります。

同じ年の異なる月にまたがる範囲の場合、両方の月を表示しますが、最初の日付から年を省略できます:「2024年12月25日~2025年1月2日」には完全な情報が必要ですが、「2024年1月15日~2月20日」は一部のロケールでは最初の日付から年を省略できます。

複数年にまたがる範囲の場合、両方の日付に年を含める必要があります:「2023年12月1日~2024年3月15日」。

これらのルールを手動で実装するには、どの日付コンポーネントが異なるかを確認し、それに応じてフォーマット文字列を構築する必要があります。異なるロケールでは、異なる区切り文字と順序規則を使用して、これらのルールを異なる方法で適用します。formatRange()メソッドは、このロジックをカプセル化します。

formatRangeを使用した日付範囲のフォーマット

formatRange()メソッドは、2つのDateオブジェクトを受け取り、フォーマットされた文字列を返します。目的のロケールとオプションでIntl.DateTimeFormatインスタンスを作成し、開始日と終了日を指定してformatRange()を呼び出します。

const formatter = new Intl.DateTimeFormat("en-US");

const start = new Date(2024, 0, 1);
const end = new Date(2024, 0, 5);

console.log(formatter.formatRange(start, end));
// Output: "1/1/24 – 1/5/24"

フォーマッターは、デフォルトの米国英語の日付フォーマットを両方の日付に適用し、enダッシュで接続します。同じ月内の日付の場合、デフォルトのフォーマットが非常に短いため、出力には両方の完全な日付が表示されます。

通常の日付フォーマットで使用可能な同じオプションを使用して、含める日付コンポーネントを指定できます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const start = new Date(2024, 0, 1);
const end = new Date(2024, 0, 5);

console.log(formatter.formatRange(start, end));
// Output: "January 1 – 5, 2024"

フォーマッターは、最初の日付と一致するため、2番目の日付から月と年をインテリジェントに省略します。出力には終了日の日付番号のみが表示され、範囲がより読みやすくなります。

formatRangeによる日付範囲出力の最適化

formatRange()メソッドは両方の日付を検証し、どの要素が異なるかを判断します。範囲の各部分には必要な要素のみが含まれます。

同じ月と年の日付の場合、2番目の部分には終了日のみが表示されます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "January 1 – 5, 2024"

console.log(formatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 0, 20)
));
// Output: "January 15 – 20, 2024"

両方の範囲は月と年を1回表示し、日付の数字のみが範囲区切り文字で接続されます。

同じ年の異なる月の日付の場合、両方の月が表示されますが、年は1回のみ表示されます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Output: "January 15 – February 20, 2024"

フォーマッターは両方の月名を含みますが、年を末尾に配置し、範囲全体に適用します。

異なる年にまたがる日付の場合、両方の完全な日付が表示されます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2023, 11, 25),
  new Date(2024, 0, 5)
));
// Output: "December 25, 2023 – January 5, 2024"

年が異なるため、各日付には完全な年が含まれます。フォーマッターはどちらの年も省略すると曖昧さが生じるため省略できません。

日付と時刻の範囲のフォーマット

フォーマットに時刻要素が含まれる場合、formatRange()メソッドは時刻フィールドにも同じインテリジェントな省略を適用します。

同じ日の時刻の場合、出力では時刻要素のみが異なります。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 1, 14, 30);

console.log(formatter.formatRange(start, end));
// Output: "January 1, 2024, 10:00 AM – 2:30 PM"

日付が1回表示され、その後に時刻範囲が続きます。フォーマッターは、終了時刻の完全な日付と時刻を繰り返しても有用な情報が追加されないことを認識しています。

異なる日の時刻の場合、両方の完全な日付と時刻の値が表示されます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 2, 14, 30);

console.log(formatter.formatRange(start, end));
// Output: "January 1, 2024, 10:00 AM – January 2, 2024, 2:30 PM"

異なる日を表すため、両方の日付と時刻が表示されます。フォーマッターはどの要素も安全に省略できません。

異なるロケールでの日付範囲のフォーマット

範囲のフォーマットは、各ロケールの日付要素の順序、区切り文字、および省略する要素の規則に適応します。

const enFormatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(enFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "January 1 – 5, 2024"

const deFormatter = new Intl.DateTimeFormat("de-DE", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(deFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "1.–5. Januar 2024"

const jaFormatter = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(jaFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "2024年1月1日~5日"

英語では月を最初に配置し、年を最後に表示します。ドイツ語では日付の数字を最初に配置してピリオドを付け、その後に月名、年の順になります。日本語では年-月-日の順序を使用し、範囲の区切り文字として波ダッシュ(~)を使用します。各ロケールは、どのコンポーネントを1回表示するか2回表示するかについて、独自の規則を適用します。

これらの違いは、月をまたぐ範囲にも及びます。

const enFormatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(enFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Output: "January 15 – February 20, 2024"

const deFormatter = new Intl.DateTimeFormat("de-DE", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(deFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Output: "15. Januar – 20. Februar 2024"

const frFormatter = new Intl.DateTimeFormat("fr-FR", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(frFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Output: "15 janvier – 20 février 2024"

3つのロケールすべてで両方の月名が表示されますが、日付の数字に対する位置が異なります。フォーマッターはこれらのバリエーションを自動的に処理します。

異なるスタイルでの日付範囲のフォーマット

dateStyleオプションを使用して、単一の日付フォーマットと同様に、全体的なフォーマットの長さを制御できます。

const shortFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short"
});

console.log(shortFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "1/1/24 – 1/5/24"

const mediumFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium"
});

console.log(mediumFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "Jan 1 – 5, 2024"

const longFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "long"
});

console.log(longFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "January 1 – 5, 2024"

const fullFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "full"
});

console.log(fullFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Output: "Monday, January 1 – Friday, January 5, 2024"

shortスタイルは数値形式の日付を生成し、フォーマットがすでにコンパクトであるため、インテリジェントな省略を適用しません。mediumおよびlongスタイルは月を省略形または完全形で表示し、冗長なコンポーネントを省略します。fullスタイルは両方の日付に曜日名を含みます。

カスタムスタイリングのためのformatRangeToPartsの使用

formatRangeToParts()メソッドは、フォーマットされた範囲のコンポーネントを表すオブジェクトの配列を返します。これにより、範囲出力の個々の部分をスタイル設定または操作できます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const parts = formatter.formatRangeToParts(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
);

console.log(parts);

出力は、それぞれがtypevaluesourceプロパティを持つオブジェクトの配列です。

[
  { type: "month", value: "January", source: "startRange" },
  { type: "literal", value: " ", source: "startRange" },
  { type: "day", value: "1", source: "startRange" },
  { type: "literal", value: " – ", source: "shared" },
  { type: "day", value: "5", source: "endRange" },
  { type: "literal", value: ", ", source: "shared" },
  { type: "year", value: "2024", source: "shared" }
]

typeプロパティはコンポーネントを識別します:月、日、年、またはリテラルテキスト。valueプロパティにはフォーマットされたテキストが含まれます。sourceプロパティは、コンポーネントが開始日、終了日に属するか、またはそれらの間で共有されているかを示します。

これらのパーツを使用して、さまざまなコンポーネントにスタイルを適用したカスタムHTMLを作成できます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const parts = formatter.formatRangeToParts(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
);

let html = "";

parts.forEach(part => {
  if (part.type === "month") {
    html += `<span class="month">${part.value}</span>`;
  } else if (part.type === "day") {
    html += `<span class="day">${part.value}</span>`;
  } else if (part.type === "year") {
    html += `<span class="year">${part.value}</span>`;
  } else if (part.type === "literal" && part.source === "shared" && part.value.includes("–")) {
    html += `<span class="separator">${part.value}</span>`;
  } else {
    html += part.value;
  }
});

console.log(html);
// Output: <span class="month">January</span> <span class="day">1</span><span class="separator"> – </span><span class="day">5</span>, <span class="year">2024</span>

この手法により、ロケール固有の書式を保持しながら、カスタムビジュアルスタイルを適用できます。

日付が等しい場合の動作

開始パラメータと終了パラメータに同じ日付を渡すと、formatRange()は範囲ではなく、単一の書式設定された日付を出力します。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2024, 0, 1);

console.log(formatter.formatRange(date, date));
// Output: "January 1, 2024"

フォーマッタは、同一の端点を持つ範囲は真の範囲ではないと認識し、単一の日付として書式設定します。この動作は、同じ値を持つ異なるDateオブジェクトのインスタンスであっても適用されます。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 1, 10, 0);

console.log(formatter.formatRange(start, end));
// Output: "January 1, 2024"

これらは別々のDateオブジェクトですが、同じ日付と時刻を表しています。フォーマットオプションの精度レベルで範囲の期間がゼロであるため、フォーマッタは単一の日付を出力します。フォーマットに時刻コンポーネントが含まれていないため、時刻は無関係であり、日付は等しいと見なされます。

formatRangeと手動書式設定の使い分け

ユーザーに日付範囲を表示する場合はformatRange()を使用します。これは、予約日付範囲、イベント期間、レポート期間、利用可能時間帯、またはその他の期間に適用されます。このメソッドは、正しいロケール固有の書式設定と最適なコンポーネントの省略を保証します。

関連性のない複数の日付を表示する必要がある場合は、formatRange()を使用しないでください。「1月1日、1月15日、2月1日」のような期限のリストは、範囲として扱うのではなく、各日付に対して通常のformat()呼び出しを使用する必要があります。

また、日付間の比較や差異を表示する場合もformatRange()を使用しないでください。ある日付が別の日付と比較してどれだけ早いか遅いかを表示する場合、それは範囲ではなく相対時間の計算を表します。