@typescript-calendar-lib/core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@typescript-calendar-lib/core",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "peerDependencies": {
8
+ "typescript": "^5"
9
+ }
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export {
2
+ getMonthName,
3
+ getWeekdayHeaders,
4
+ LOCALES,
5
+ } from "./locale.ts";
6
+ export type {
7
+ CalendarOptions,
8
+ CalendarRangeOptions,
9
+ CalendarYearOptions,
10
+ HighlightStyle,
11
+ Locale,
12
+ RenderMonthOptions,
13
+ WeekStart,
14
+ } from "./types.ts";
15
+ export {
16
+ buildMonthGrid,
17
+ firstDayOfMonth,
18
+ getMonthRange,
19
+ isDateInRange,
20
+ isSameDay,
21
+ lastDayOfMonth,
22
+ } from "./utils.ts";
@@ -0,0 +1,175 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { getMonthName, getWeekdayHeaders, LOCALES } from "./locale.ts";
3
+
4
+ describe("LOCALES", () => {
5
+ test("全ロケールが12ヶ月分の月名を持つ", () => {
6
+ for (const locale of Object.keys(LOCALES) as (keyof typeof LOCALES)[]) {
7
+ expect(LOCALES[locale].months).toHaveLength(12);
8
+ }
9
+ });
10
+
11
+ test("全ロケールが7曜日のヘッダを持つ", () => {
12
+ for (const locale of Object.keys(LOCALES) as (keyof typeof LOCALES)[]) {
13
+ expect(LOCALES[locale].weekdays).toHaveLength(7);
14
+ expect(LOCALES[locale].weekdaysShort).toHaveLength(7);
15
+ expect(LOCALES[locale].weekdaysMonday).toHaveLength(7);
16
+ expect(LOCALES[locale].weekdaysMondayShort).toHaveLength(7);
17
+ }
18
+ });
19
+
20
+ test("日本語以外のロケールが存在する", () => {
21
+ expect(Object.keys(LOCALES)).toEqual(
22
+ expect.arrayContaining(["en", "ja", "es", "de", "fr", "ko", "zh"]),
23
+ );
24
+ });
25
+ });
26
+
27
+ describe("getWeekdayHeaders", () => {
28
+ test("英語・日曜始まりはSunから", () => {
29
+ expect(getWeekdayHeaders("en", "sunday")).toEqual([
30
+ "Sun",
31
+ "Mon",
32
+ "Tue",
33
+ "Wed",
34
+ "Thu",
35
+ "Fri",
36
+ "Sat",
37
+ ]);
38
+ });
39
+
40
+ test("英語・月曜始まりはMonから", () => {
41
+ expect(getWeekdayHeaders("en", "monday")).toEqual([
42
+ "Mon",
43
+ "Tue",
44
+ "Wed",
45
+ "Thu",
46
+ "Fri",
47
+ "Sat",
48
+ "Sun",
49
+ ]);
50
+ });
51
+
52
+ test("日本語・日曜始まりは日から", () => {
53
+ expect(getWeekdayHeaders("ja", "sunday")).toEqual([
54
+ "日",
55
+ "月",
56
+ "火",
57
+ "水",
58
+ "木",
59
+ "金",
60
+ "土",
61
+ ]);
62
+ });
63
+
64
+ test("日本語・月曜始まりは月から", () => {
65
+ expect(getWeekdayHeaders("ja", "monday")).toEqual([
66
+ "月",
67
+ "火",
68
+ "水",
69
+ "木",
70
+ "金",
71
+ "土",
72
+ "日",
73
+ ]);
74
+ });
75
+
76
+ test("スペイン語・日曜始まり", () => {
77
+ expect(getWeekdayHeaders("es", "sunday")).toEqual([
78
+ "dom",
79
+ "lun",
80
+ "mar",
81
+ "mié",
82
+ "jue",
83
+ "vie",
84
+ "sáb",
85
+ ]);
86
+ });
87
+
88
+ test("ドイツ語・月曜始まり", () => {
89
+ expect(getWeekdayHeaders("de", "monday")).toEqual([
90
+ "Mo",
91
+ "Di",
92
+ "Mi",
93
+ "Do",
94
+ "Fr",
95
+ "Sa",
96
+ "So",
97
+ ]);
98
+ });
99
+
100
+ test("フランス語・日曜始まり", () => {
101
+ expect(getWeekdayHeaders("fr", "sunday")).toEqual([
102
+ "dim.",
103
+ "lun.",
104
+ "mar.",
105
+ "mer.",
106
+ "jeu.",
107
+ "ven.",
108
+ "sam.",
109
+ ]);
110
+ });
111
+
112
+ test("韓国語・月曜始まり", () => {
113
+ expect(getWeekdayHeaders("ko", "monday")).toEqual([
114
+ "월",
115
+ "화",
116
+ "수",
117
+ "목",
118
+ "금",
119
+ "토",
120
+ "일",
121
+ ]);
122
+ });
123
+
124
+ test("中国語・月曜始まり", () => {
125
+ expect(getWeekdayHeaders("zh", "monday")).toEqual([
126
+ "一",
127
+ "二",
128
+ "三",
129
+ "四",
130
+ "五",
131
+ "六",
132
+ "日",
133
+ ]);
134
+ });
135
+ });
136
+
137
+ describe("getMonthName", () => {
138
+ test("英語の月名", () => {
139
+ expect(getMonthName("en", 1)).toBe("January");
140
+ expect(getMonthName("en", 9)).toBe("September");
141
+ expect(getMonthName("en", 12)).toBe("December");
142
+ });
143
+
144
+ test("日本語の月名", () => {
145
+ expect(getMonthName("ja", 1)).toBe("1月");
146
+ expect(getMonthName("ja", 12)).toBe("12月");
147
+ });
148
+
149
+ test("スペイン語の月名", () => {
150
+ expect(getMonthName("es", 1)).toBe("enero");
151
+ expect(getMonthName("es", 9)).toBe("septiembre");
152
+ expect(getMonthName("es", 12)).toBe("diciembre");
153
+ });
154
+
155
+ test("ドイツ語の月名", () => {
156
+ expect(getMonthName("de", 1)).toBe("Januar");
157
+ expect(getMonthName("de", 3)).toBe("März");
158
+ expect(getMonthName("de", 12)).toBe("Dezember");
159
+ });
160
+
161
+ test("フランス語の月名", () => {
162
+ expect(getMonthName("fr", 1)).toBe("janvier");
163
+ expect(getMonthName("fr", 8)).toBe("août");
164
+ });
165
+
166
+ test("韓国語の月名", () => {
167
+ expect(getMonthName("ko", 1)).toBe("1월");
168
+ expect(getMonthName("ko", 12)).toBe("12월");
169
+ });
170
+
171
+ test("中国語の月名", () => {
172
+ expect(getMonthName("zh", 1)).toBe("一月");
173
+ expect(getMonthName("zh", 12)).toBe("十二月");
174
+ });
175
+ });
package/src/locale.ts ADDED
@@ -0,0 +1,164 @@
1
+ import type { Locale, WeekStart } from "./types.ts";
2
+
3
+ export interface LocaleData {
4
+ months: readonly string[];
5
+ weekdays: readonly string[];
6
+ weekdaysShort: readonly string[];
7
+ weekdaysMonday: readonly string[];
8
+ weekdaysMondayShort: readonly string[];
9
+ }
10
+
11
+ export const LOCALES: Record<Locale, LocaleData> = {
12
+ en: {
13
+ months: [
14
+ "January",
15
+ "February",
16
+ "March",
17
+ "April",
18
+ "May",
19
+ "June",
20
+ "July",
21
+ "August",
22
+ "September",
23
+ "October",
24
+ "November",
25
+ "December",
26
+ ],
27
+ weekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
28
+ weekdaysShort: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
29
+ weekdaysMonday: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
30
+ weekdaysMondayShort: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
31
+ },
32
+ ja: {
33
+ months: [
34
+ "1月",
35
+ "2月",
36
+ "3月",
37
+ "4月",
38
+ "5月",
39
+ "6月",
40
+ "7月",
41
+ "8月",
42
+ "9月",
43
+ "10月",
44
+ "11月",
45
+ "12月",
46
+ ],
47
+ weekdays: ["日", "月", "火", "水", "木", "金", "土"],
48
+ weekdaysShort: ["日", "月", "火", "水", "木", "金", "土"],
49
+ weekdaysMonday: ["月", "火", "水", "木", "金", "土", "日"],
50
+ weekdaysMondayShort: ["月", "火", "水", "木", "金", "土", "日"],
51
+ },
52
+ es: {
53
+ months: [
54
+ "enero",
55
+ "febrero",
56
+ "marzo",
57
+ "abril",
58
+ "mayo",
59
+ "junio",
60
+ "julio",
61
+ "agosto",
62
+ "septiembre",
63
+ "octubre",
64
+ "noviembre",
65
+ "diciembre",
66
+ ],
67
+ weekdays: ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"],
68
+ weekdaysShort: ["do", "lu", "ma", "mi", "ju", "vi", "sá"],
69
+ weekdaysMonday: ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"],
70
+ weekdaysMondayShort: ["lu", "ma", "mi", "ju", "vi", "sá", "do"],
71
+ },
72
+ de: {
73
+ months: [
74
+ "Januar",
75
+ "Februar",
76
+ "März",
77
+ "April",
78
+ "Mai",
79
+ "Juni",
80
+ "Juli",
81
+ "August",
82
+ "September",
83
+ "Oktober",
84
+ "November",
85
+ "Dezember",
86
+ ],
87
+ weekdays: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
88
+ weekdaysShort: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
89
+ weekdaysMonday: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
90
+ weekdaysMondayShort: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
91
+ },
92
+ fr: {
93
+ months: [
94
+ "janvier",
95
+ "février",
96
+ "mars",
97
+ "avril",
98
+ "mai",
99
+ "juin",
100
+ "juillet",
101
+ "août",
102
+ "septembre",
103
+ "octobre",
104
+ "novembre",
105
+ "décembre",
106
+ ],
107
+ weekdays: ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
108
+ weekdaysShort: ["di", "lu", "ma", "me", "je", "ve", "sa"],
109
+ weekdaysMonday: ["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."],
110
+ weekdaysMondayShort: ["lu", "ma", "me", "je", "ve", "sa", "di"],
111
+ },
112
+ ko: {
113
+ months: [
114
+ "1월",
115
+ "2월",
116
+ "3월",
117
+ "4월",
118
+ "5월",
119
+ "6월",
120
+ "7월",
121
+ "8월",
122
+ "9월",
123
+ "10월",
124
+ "11월",
125
+ "12월",
126
+ ],
127
+ weekdays: ["일", "월", "화", "수", "목", "금", "토"],
128
+ weekdaysShort: ["일", "월", "화", "수", "목", "금", "토"],
129
+ weekdaysMonday: ["월", "화", "수", "목", "금", "토", "일"],
130
+ weekdaysMondayShort: ["월", "화", "수", "목", "금", "토", "일"],
131
+ },
132
+ zh: {
133
+ months: [
134
+ "一月",
135
+ "二月",
136
+ "三月",
137
+ "四月",
138
+ "五月",
139
+ "六月",
140
+ "七月",
141
+ "八月",
142
+ "九月",
143
+ "十月",
144
+ "十一月",
145
+ "十二月",
146
+ ],
147
+ weekdays: ["日", "一", "二", "三", "四", "五", "六"],
148
+ weekdaysShort: ["日", "一", "二", "三", "四", "五", "六"],
149
+ weekdaysMonday: ["一", "二", "三", "四", "五", "六", "日"],
150
+ weekdaysMondayShort: ["一", "二", "三", "四", "五", "六", "日"],
151
+ },
152
+ };
153
+
154
+ export function getWeekdayHeaders(
155
+ locale: Locale,
156
+ weekStart: WeekStart,
157
+ ): readonly string[] {
158
+ const data = LOCALES[locale];
159
+ return weekStart === "monday" ? data.weekdaysMonday : data.weekdays;
160
+ }
161
+
162
+ export function getMonthName(locale: Locale, month: number): string {
163
+ return LOCALES[locale].months[month - 1]!;
164
+ }
package/src/types.ts ADDED
@@ -0,0 +1,44 @@
1
+ export type Locale = "en" | "ja" | "es" | "de" | "fr" | "ko" | "zh";
2
+ export type WeekStart = "sunday" | "monday";
3
+ export type HighlightStyle = "bracket" | "reverse";
4
+
5
+ export interface CalendarOptions {
6
+ year: number;
7
+ month: number;
8
+ locale?: Locale;
9
+ weekStart?: WeekStart;
10
+ highlight?: Date;
11
+ highlightStyle?: HighlightStyle;
12
+ range?: { from: Date; to: Date };
13
+ color?: boolean;
14
+ }
15
+
16
+ export interface CalendarYearOptions {
17
+ year: number;
18
+ locale?: Locale;
19
+ weekStart?: WeekStart;
20
+ highlight?: Date;
21
+ highlightStyle?: HighlightStyle;
22
+ range?: { from: Date; to: Date };
23
+ color?: boolean;
24
+ }
25
+
26
+ export interface CalendarRangeOptions {
27
+ from: Date;
28
+ to: Date;
29
+ locale?: Locale;
30
+ weekStart?: WeekStart;
31
+ highlight?: Date;
32
+ highlightStyle?: HighlightStyle;
33
+ range?: { from: Date; to: Date };
34
+ color?: boolean;
35
+ }
36
+
37
+ export interface RenderMonthOptions {
38
+ locale?: Locale;
39
+ weekStart?: WeekStart;
40
+ highlight?: Date;
41
+ highlightStyle?: HighlightStyle;
42
+ range?: { from: Date; to: Date };
43
+ color?: boolean;
44
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ buildMonthGrid,
4
+ firstDayOfMonth,
5
+ getMonthRange,
6
+ isDateInRange,
7
+ isSameDay,
8
+ lastDayOfMonth,
9
+ } from "./utils.ts";
10
+
11
+ describe("firstDayOfMonth", () => {
12
+ test("1月の月初", () => {
13
+ expect(firstDayOfMonth(2026, 1)).toEqual(new Date(2026, 0, 1));
14
+ });
15
+
16
+ test("12月の月初", () => {
17
+ expect(firstDayOfMonth(2026, 12)).toEqual(new Date(2026, 11, 1));
18
+ });
19
+ });
20
+
21
+ describe("lastDayOfMonth", () => {
22
+ test("30日で終わる月(4月は30日)", () => {
23
+ expect(lastDayOfMonth(2026, 4)).toEqual(new Date(2026, 3, 30));
24
+ });
25
+
26
+ test("31日で終わる月", () => {
27
+ expect(lastDayOfMonth(2026, 1)).toEqual(new Date(2026, 0, 31));
28
+ });
29
+
30
+ test("通常年の2月は28日", () => {
31
+ expect(lastDayOfMonth(2026, 2)).toEqual(new Date(2026, 1, 28));
32
+ });
33
+
34
+ test("うるう年の2月は29日", () => {
35
+ expect(lastDayOfMonth(2024, 2)).toEqual(new Date(2024, 1, 29));
36
+ });
37
+ });
38
+
39
+ describe("buildMonthGrid", () => {
40
+ test("gridは常に6行×7列", () => {
41
+ const grid = buildMonthGrid(2026, 9, "sunday");
42
+ expect(grid).toHaveLength(6);
43
+ for (const row of grid) {
44
+ expect(row).toHaveLength(7);
45
+ }
46
+ });
47
+
48
+ test("9月2026(日曜始まり)は1日が火曜から開始", () => {
49
+ const grid = buildMonthGrid(2026, 9, "sunday");
50
+ expect(grid[0]).toEqual([null, null, 1, 2, 3, 4, 5]);
51
+ });
52
+
53
+ test("9月2026(月曜始まり)は1日が月曜から開始", () => {
54
+ const grid = buildMonthGrid(2026, 9, "monday");
55
+ expect(grid[0]).toEqual([null, 1, 2, 3, 4, 5, 6]);
56
+ });
57
+
58
+ test("日付の日数が月末を超えない", () => {
59
+ const grid = buildMonthGrid(2026, 2, "sunday");
60
+ const flat = grid.flat().filter((d) => d !== null);
61
+ expect(Math.max(...(flat as number[]))).toBe(28);
62
+ expect(flat).toHaveLength(28);
63
+ });
64
+ });
65
+
66
+ describe("isDateInRange", () => {
67
+ const range = { from: new Date(2026, 8, 1), to: new Date(2026, 8, 15) };
68
+
69
+ test("範囲なしなら常にfalse", () => {
70
+ expect(isDateInRange(new Date(2026, 8, 1))).toBe(false);
71
+ });
72
+
73
+ test("範囲内の日付はtrue", () => {
74
+ expect(isDateInRange(new Date(2026, 8, 10), range)).toBe(true);
75
+ });
76
+
77
+ test("開始日は境界として含む", () => {
78
+ expect(isDateInRange(new Date(2026, 8, 1), range)).toBe(true);
79
+ });
80
+
81
+ test("終了日は境界として含む", () => {
82
+ expect(isDateInRange(new Date(2026, 8, 15), range)).toBe(true);
83
+ });
84
+
85
+ test("範囲外の前日はfalse", () => {
86
+ expect(isDateInRange(new Date(2026, 7, 31), range)).toBe(false);
87
+ });
88
+
89
+ test("範囲外の翌日はfalse", () => {
90
+ expect(isDateInRange(new Date(2026, 8, 16), range)).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe("isSameDay", () => {
95
+ test("同じ日付はtrue", () => {
96
+ expect(
97
+ isSameDay(new Date(2026, 8, 8, 3, 30), new Date(2026, 8, 8, 20, 0)),
98
+ ).toBe(true);
99
+ });
100
+
101
+ test("異なる日はfalse", () => {
102
+ expect(isSameDay(new Date(2026, 8, 8), new Date(2026, 8, 9))).toBe(false);
103
+ });
104
+
105
+ test("同じ日だが異なる月はfalse", () => {
106
+ expect(isSameDay(new Date(2026, 8, 8), new Date(2026, 7, 8))).toBe(false);
107
+ });
108
+ });
109
+
110
+ describe("getMonthRange", () => {
111
+ test("同月内は1件", () => {
112
+ expect(getMonthRange(new Date(2026, 8, 1), new Date(2026, 8, 30))).toEqual([
113
+ { year: 2026, month: 9 },
114
+ ]);
115
+ });
116
+
117
+ test("複数月に跨る", () => {
118
+ expect(getMonthRange(new Date(2026, 5, 1), new Date(2026, 8, 30))).toEqual([
119
+ { year: 2026, month: 6 },
120
+ { year: 2026, month: 7 },
121
+ { year: 2026, month: 8 },
122
+ { year: 2026, month: 9 },
123
+ ]);
124
+ });
125
+
126
+ test("12月から翌年1月に跨る", () => {
127
+ expect(
128
+ getMonthRange(new Date(2025, 11, 31), new Date(2026, 0, 15)),
129
+ ).toEqual([
130
+ { year: 2025, month: 12 },
131
+ { year: 2026, month: 1 },
132
+ ]);
133
+ });
134
+
135
+ test("1年の範囲", () => {
136
+ expect(
137
+ getMonthRange(new Date(2026, 0, 1), new Date(2026, 11, 31)),
138
+ ).toHaveLength(12);
139
+ });
140
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type { WeekStart } from "./types.ts";
2
+
3
+ /** 月初日(1日)を返す */
4
+ export function firstDayOfMonth(year: number, month: number): Date {
5
+ return new Date(year, month - 1, 1);
6
+ }
7
+
8
+ /** 月末日を返す */
9
+ export function lastDayOfMonth(year: number, month: number): Date {
10
+ return new Date(year, month, 0);
11
+ }
12
+
13
+ /**
14
+ * 月の日付グリッドを生成する(6行×7列)
15
+ * 各セルは日数(1-31)または null(空欄)
16
+ */
17
+ export function buildMonthGrid(
18
+ year: number,
19
+ month: number,
20
+ weekStart: WeekStart,
21
+ ): (number | null)[][] {
22
+ const first = firstDayOfMonth(year, month);
23
+ const last = lastDayOfMonth(year, month);
24
+ const daysInMonth = last.getDate();
25
+
26
+ let startDayOfWeek = first.getDay();
27
+ if (weekStart === "monday") {
28
+ startDayOfWeek = (startDayOfWeek + 6) % 7;
29
+ }
30
+
31
+ const grid: (number | null)[][] = [];
32
+ let currentDay = 1;
33
+
34
+ for (let week = 0; week < 6; week++) {
35
+ const row: (number | null)[] = [];
36
+ for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
37
+ if (
38
+ (week === 0 && dayOfWeek < startDayOfWeek) ||
39
+ currentDay > daysInMonth
40
+ ) {
41
+ row.push(null);
42
+ } else {
43
+ row.push(currentDay);
44
+ currentDay++;
45
+ }
46
+ }
47
+ grid.push(row);
48
+ }
49
+
50
+ return grid;
51
+ }
52
+
53
+ /** 日付が範囲内に含まれるか */
54
+ export function isDateInRange(
55
+ date: Date,
56
+ range?: { from: Date; to: Date },
57
+ ): boolean {
58
+ if (!range) return false;
59
+ const time = date.getTime();
60
+ return time >= range.from.getTime() && time <= range.to.getTime();
61
+ }
62
+
63
+ /** 日付が今日と一致するか(日付のみ比較) */
64
+ export function isSameDay(a: Date, b: Date): boolean {
65
+ return (
66
+ a.getFullYear() === b.getFullYear() &&
67
+ a.getMonth() === b.getMonth() &&
68
+ a.getDate() === b.getDate()
69
+ );
70
+ }
71
+
72
+ /** 日付が範囲の年开始から範囲の年结束までの月リストを返す */
73
+ export function getMonthRange(
74
+ from: Date,
75
+ to: Date,
76
+ ): { year: number; month: number }[] {
77
+ const months: { year: number; month: number }[] = [];
78
+ let year = from.getFullYear();
79
+ let month = from.getMonth() + 1;
80
+
81
+ while (
82
+ year < to.getFullYear() ||
83
+ (year === to.getFullYear() && month <= to.getMonth() + 1)
84
+ ) {
85
+ months.push({ year, month });
86
+ month++;
87
+ if (month > 12) {
88
+ month = 1;
89
+ year++;
90
+ }
91
+ }
92
+
93
+ return months;
94
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src"
5
+ },
6
+ "include": ["src"]
7
+ }