1月1日~1月5日のような日付範囲のフォーマット方法
JavaScriptを使用して、地域に適したフォーマットと重複を賢く省略した日付範囲を表示する
はじめに
日付範囲はウェブアプリケーション全体に表示されています。予約システムでは1月1日から1月5日までの空き状況を表示し、イベントカレンダーでは複数日にわたる会議を表示し、分析ダッシュボードでは特定の期間のデータを表示し、レポートでは会計四半期やカスタム日付範囲をカバーしています。これらの範囲は、2つの端点の間のすべての日付に何かが適用されることを伝えています。
2つのフォーマットされた日付をダッシュで連結して日付範囲を手動でフォーマットすると、不必要に冗長な出力が作成されます。2024年1月1日から2024年1月5日までの範囲では、2番目の日付に月と年を繰り返す必要はありません。「2024年1月1日~5日」という出力は、冗長なコンポーネントを省略することで、同じ情報をより簡潔に伝えます。
JavaScriptは、Intl.DateTimeFormatのformatRange()メソッドを提供し、日付範囲のフォーマットを自動的に処理します。このメソッドは、範囲の各部分に含める日付コンポーネントを決定し、不要な繰り返しを削除しながら、区切り文字とフォーマットのロケール固有の規則を適用します。
日付範囲がインテリジェントなフォーマットを必要とする理由
異なる日付範囲には異なるレベルの詳細が必要です。同じ月内の範囲は、複数年にまたがる範囲よりも少ない情報で済みます。最適なフォーマットは、開始日と終了日の間で異なる日付コンポーネントによって決まります。
同じ日内の範囲では、時間差のみを表示する必要があります:「午前10時~午後2時」。両方の時間に完全な日付を繰り返しても情報は追加されません。
同じ月内の範囲では、月を一度だけ表示し、両方の日数を列挙します:「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));
// 出力: "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));
// 出力: "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)
));
// 出力: "January 1 – 5, 2024"
console.log(formatter.formatRange(
new Date(2024, 0, 15),
new Date(2024, 0, 20)
));
// 出力: "January 15 – 20, 2024"
両方の範囲で、月と年は一度だけ表示され、日付番号だけが範囲区切り記号で接続されています。
同じ年の異なる月の日付の場合、両方の月が表示されますが、年は一度だけ表示されます。
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)
));
// 出力: "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)
));
// 出力: "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));
// 出力: "January 1, 2024, 10:00 AM – 2:30 PM"
日付は一度だけ表示され、その後に時間範囲が続きます。フォーマッタは、終了時の完全な日付と時間を繰り返すことが有用な情報を追加しないことを認識しています。
異なる日の時間については、完全な日付と時間の値の両方が表示されます。
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));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "2024年1月1日~5日"
英語では月を最初に配置し、年を最後に表示します。ドイツ語では日数を最初にピリオド付きで配置し、次に月名、そして年を表示します。日本語では年-月-日の順序と波ダッシュ(~)を範囲区切り文字として使用します。各ロケールは、どのコンポーネントを一度表示するか二度表示するかについて独自の規則を適用します。
これらの違いは月をまたぐ範囲にも及びます。
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)
));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "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)
));
// 出力: "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);
出力は、type、value、およびsourceプロパティを持つオブジェクトの配列です。
[
{ 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);
// 出力: <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));
// 出力: "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));
// 出力: "January 1, 2024"
これらは別々のDateオブジェクトですが、同じ日付と時刻を表しています。フォーマットオプションの精度レベルでは範囲の期間がゼロであるため、フォーマッタは単一の日付を出力します。フォーマットに時間コンポーネントが含まれていないため、時間は無関係であり、日付は同等と見なされます。
formatRangeと手動フォーマットの使い分け
ユーザーに日付範囲を表示する場合はformatRange()を使用してください。これは予約日範囲、イベント期間、レポート期間、利用可能時間枠、またはその他の時間範囲に適用されます。このメソッドは、ロケール固有のフォーマットと最適なコンポーネント省略を保証します。
複数の無関係な日付を表示する必要がある場合は、formatRange()を避けてください。「1月1日、1月15日、2月1日」のような期限のリストは、それらを範囲として扱うのではなく、各日付に対して通常のformat()呼び出しを使用すべきです。
また、日付間の比較や差異を表示する場合もformatRange()を避けてください。ある日付が別の日付と比較してどれだけ早いまたは遅いかを表示する場合、それは範囲ではなく相対的な時間計算を表します。