react-native-month-day-picker 0.1.0

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/lib/commonjs/BirthdayPicker.js +177 -0
  4. package/lib/commonjs/BirthdayPicker.js.map +1 -0
  5. package/lib/commonjs/BirthdayPickerModal.js +176 -0
  6. package/lib/commonjs/BirthdayPickerModal.js.map +1 -0
  7. package/lib/commonjs/constants.js +80 -0
  8. package/lib/commonjs/constants.js.map +1 -0
  9. package/lib/commonjs/hooks/useBirthdayPicker.js +90 -0
  10. package/lib/commonjs/hooks/useBirthdayPicker.js.map +1 -0
  11. package/lib/commonjs/index.js +89 -0
  12. package/lib/commonjs/index.js.map +1 -0
  13. package/lib/commonjs/types.js +6 -0
  14. package/lib/commonjs/types.js.map +1 -0
  15. package/lib/commonjs/utils/dateUtils.js +103 -0
  16. package/lib/commonjs/utils/dateUtils.js.map +1 -0
  17. package/lib/commonjs/utils/localeUtils.js +90 -0
  18. package/lib/commonjs/utils/localeUtils.js.map +1 -0
  19. package/lib/module/BirthdayPicker.js +169 -0
  20. package/lib/module/BirthdayPicker.js.map +1 -0
  21. package/lib/module/BirthdayPickerModal.js +169 -0
  22. package/lib/module/BirthdayPickerModal.js.map +1 -0
  23. package/lib/module/constants.js +74 -0
  24. package/lib/module/constants.js.map +1 -0
  25. package/lib/module/hooks/useBirthdayPicker.js +84 -0
  26. package/lib/module/hooks/useBirthdayPicker.js.map +1 -0
  27. package/lib/module/index.js +16 -0
  28. package/lib/module/index.js.map +1 -0
  29. package/lib/module/types.js +2 -0
  30. package/lib/module/types.js.map +1 -0
  31. package/lib/module/utils/dateUtils.js +92 -0
  32. package/lib/module/utils/dateUtils.js.map +1 -0
  33. package/lib/module/utils/localeUtils.js +81 -0
  34. package/lib/module/utils/localeUtils.js.map +1 -0
  35. package/lib/typescript/BirthdayPicker.d.ts +25 -0
  36. package/lib/typescript/BirthdayPicker.d.ts.map +1 -0
  37. package/lib/typescript/BirthdayPickerModal.d.ts +24 -0
  38. package/lib/typescript/BirthdayPickerModal.d.ts.map +1 -0
  39. package/lib/typescript/constants.d.ts +39 -0
  40. package/lib/typescript/constants.d.ts.map +1 -0
  41. package/lib/typescript/hooks/useBirthdayPicker.d.ts +17 -0
  42. package/lib/typescript/hooks/useBirthdayPicker.d.ts.map +1 -0
  43. package/lib/typescript/index.d.ts +8 -0
  44. package/lib/typescript/index.d.ts.map +1 -0
  45. package/lib/typescript/types.d.ts +160 -0
  46. package/lib/typescript/types.d.ts.map +1 -0
  47. package/lib/typescript/utils/dateUtils.d.ts +43 -0
  48. package/lib/typescript/utils/dateUtils.d.ts.map +1 -0
  49. package/lib/typescript/utils/localeUtils.d.ts +28 -0
  50. package/lib/typescript/utils/localeUtils.d.ts.map +1 -0
  51. package/package.json +137 -0
  52. package/src/BirthdayPicker.tsx +210 -0
  53. package/src/BirthdayPickerModal.tsx +192 -0
  54. package/src/constants.ts +64 -0
  55. package/src/hooks/useBirthdayPicker.ts +106 -0
  56. package/src/index.ts +31 -0
  57. package/src/types.ts +189 -0
  58. package/src/utils/dateUtils.ts +101 -0
  59. package/src/utils/localeUtils.ts +99 -0
@@ -0,0 +1,192 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Text,
6
+ TouchableOpacity,
7
+ StyleSheet,
8
+ SafeAreaView,
9
+ Platform,
10
+ } from 'react-native';
11
+ import { BirthdayPicker } from './BirthdayPicker';
12
+ import type { BirthdayPickerModalProps, BirthdayValue } from './types';
13
+ import { DEFAULT_BIRTHDAY_VALUE } from './constants';
14
+
15
+ /**
16
+ * A modal wrapper for BirthdayPicker that provides confirm/cancel functionality.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * const [visible, setVisible] = useState(false);
21
+ * const [birthday, setBirthday] = useState({ month: 1, day: 1 });
22
+ *
23
+ * <BirthdayPickerModal
24
+ * visible={visible}
25
+ * value={birthday}
26
+ * onConfirm={(value) => {
27
+ * setBirthday(value);
28
+ * setVisible(false);
29
+ * }}
30
+ * onCancel={() => setVisible(false)}
31
+ * />
32
+ * ```
33
+ */
34
+ export function BirthdayPickerModal({
35
+ visible,
36
+ onConfirm,
37
+ onCancel,
38
+ value: externalValue,
39
+ defaultValue = DEFAULT_BIRTHDAY_VALUE,
40
+ title = 'Select Birthday',
41
+ confirmText = 'Confirm',
42
+ cancelText = 'Cancel',
43
+ animationType = 'slide',
44
+ locale,
45
+ monthFormat,
46
+ allowLeapDay,
47
+ disabled,
48
+ testID,
49
+ itemHeight,
50
+ visibleItems,
51
+ monthAccessibilityLabel,
52
+ dayAccessibilityLabel,
53
+ }: BirthdayPickerModalProps): React.ReactElement {
54
+ // Internal state to track the selection while modal is open
55
+ const [internalValue, setInternalValue] = useState<BirthdayValue>(
56
+ () => externalValue ?? defaultValue
57
+ );
58
+
59
+ // Reset internal value when modal opens or external value changes
60
+ useEffect(() => {
61
+ if (visible) {
62
+ setInternalValue(externalValue ?? defaultValue);
63
+ }
64
+ }, [visible, externalValue, defaultValue]);
65
+
66
+ // Handle value changes from the picker
67
+ const handleChange = useCallback((newValue: BirthdayValue) => {
68
+ setInternalValue(newValue);
69
+ }, []);
70
+
71
+ // Handle confirm button press
72
+ const handleConfirm = useCallback(() => {
73
+ onConfirm(internalValue);
74
+ }, [onConfirm, internalValue]);
75
+
76
+ // Handle cancel button press
77
+ const handleCancel = useCallback(() => {
78
+ onCancel();
79
+ }, [onCancel]);
80
+
81
+ return (
82
+ <Modal
83
+ visible={visible}
84
+ transparent
85
+ animationType={animationType}
86
+ onRequestClose={handleCancel}
87
+ testID={testID}
88
+ >
89
+ <View style={styles.overlay}>
90
+ <SafeAreaView style={styles.safeArea}>
91
+ <View style={styles.container}>
92
+ {/* Header */}
93
+ <View style={styles.header}>
94
+ <TouchableOpacity
95
+ onPress={handleCancel}
96
+ style={styles.headerButton}
97
+ accessibilityRole="button"
98
+ accessibilityLabel={cancelText}
99
+ testID={testID ? `${testID}-cancel` : undefined}
100
+ >
101
+ <Text style={styles.cancelText}>{cancelText}</Text>
102
+ </TouchableOpacity>
103
+
104
+ <Text style={styles.title}>{title}</Text>
105
+
106
+ <TouchableOpacity
107
+ onPress={handleConfirm}
108
+ style={styles.headerButton}
109
+ accessibilityRole="button"
110
+ accessibilityLabel={confirmText}
111
+ testID={testID ? `${testID}-confirm` : undefined}
112
+ >
113
+ <Text style={styles.confirmText}>{confirmText}</Text>
114
+ </TouchableOpacity>
115
+ </View>
116
+
117
+ {/* Picker */}
118
+ <View style={styles.pickerWrapper}>
119
+ <BirthdayPicker
120
+ value={internalValue}
121
+ onChange={handleChange}
122
+ locale={locale}
123
+ monthFormat={monthFormat}
124
+ allowLeapDay={allowLeapDay}
125
+ disabled={disabled}
126
+ itemHeight={itemHeight}
127
+ visibleItems={visibleItems}
128
+ monthAccessibilityLabel={monthAccessibilityLabel}
129
+ dayAccessibilityLabel={dayAccessibilityLabel}
130
+ testID={testID ? `${testID}-picker` : undefined}
131
+ />
132
+ </View>
133
+ </View>
134
+ </SafeAreaView>
135
+ </View>
136
+ </Modal>
137
+ );
138
+ }
139
+
140
+ const styles = StyleSheet.create({
141
+ overlay: {
142
+ flex: 1,
143
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
144
+ justifyContent: 'flex-end',
145
+ },
146
+ safeArea: {
147
+ backgroundColor: 'white',
148
+ borderTopLeftRadius: 16,
149
+ borderTopRightRadius: 16,
150
+ overflow: 'hidden',
151
+ },
152
+ container: {
153
+ backgroundColor: 'white',
154
+ paddingBottom: Platform.OS === 'android' ? 16 : 0,
155
+ },
156
+ header: {
157
+ flexDirection: 'row',
158
+ alignItems: 'center',
159
+ justifyContent: 'space-between',
160
+ paddingHorizontal: 16,
161
+ paddingVertical: 12,
162
+ borderBottomWidth: StyleSheet.hairlineWidth,
163
+ borderBottomColor: '#ccc',
164
+ },
165
+ headerButton: {
166
+ paddingVertical: 8,
167
+ paddingHorizontal: 4,
168
+ minWidth: 60,
169
+ },
170
+ title: {
171
+ fontSize: 17,
172
+ fontWeight: '600',
173
+ color: '#000',
174
+ textAlign: 'center',
175
+ flex: 1,
176
+ },
177
+ cancelText: {
178
+ fontSize: 17,
179
+ color: '#007AFF',
180
+ },
181
+ confirmText: {
182
+ fontSize: 17,
183
+ fontWeight: '600',
184
+ color: '#007AFF',
185
+ textAlign: 'right',
186
+ },
187
+ pickerWrapper: {
188
+ paddingVertical: 16,
189
+ },
190
+ });
191
+
192
+ export default BirthdayPickerModal;
@@ -0,0 +1,64 @@
1
+ import type { BirthdayValue, MonthFormat } from './types';
2
+
3
+ /**
4
+ * Default birthday value (January 1st)
5
+ */
6
+ export const DEFAULT_BIRTHDAY_VALUE: BirthdayValue = {
7
+ month: 1,
8
+ day: 1,
9
+ };
10
+
11
+ /**
12
+ * Default locale for month formatting
13
+ */
14
+ export const DEFAULT_LOCALE = 'en-US';
15
+
16
+ /**
17
+ * Default month format
18
+ */
19
+ export const DEFAULT_MONTH_FORMAT: MonthFormat = 'long';
20
+
21
+ /**
22
+ * Default item height in pixels
23
+ */
24
+ export const DEFAULT_ITEM_HEIGHT = 40;
25
+
26
+ /**
27
+ * Default number of visible items
28
+ */
29
+ export const DEFAULT_VISIBLE_ITEMS = 5;
30
+
31
+ /**
32
+ * Days in each month (1-indexed, index 0 is unused)
33
+ * February is set to 28, leap day handling is separate
34
+ */
35
+ export const DAYS_IN_MONTH: readonly number[] = [
36
+ 0, // Index 0 unused
37
+ 31, // January
38
+ 28, // February (leap day handled separately)
39
+ 31, // March
40
+ 30, // April
41
+ 31, // May
42
+ 30, // June
43
+ 31, // July
44
+ 31, // August
45
+ 30, // September
46
+ 31, // October
47
+ 30, // November
48
+ 31, // December
49
+ ];
50
+
51
+ /**
52
+ * Number of months in a year
53
+ */
54
+ export const MONTHS_IN_YEAR = 12;
55
+
56
+ /**
57
+ * February month number
58
+ */
59
+ export const FEBRUARY = 2;
60
+
61
+ /**
62
+ * Leap day (Feb 29)
63
+ */
64
+ export const LEAP_DAY = 29;
@@ -0,0 +1,106 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import type {
3
+ BirthdayValue,
4
+ UseBirthdayPickerOptions,
5
+ UseBirthdayPickerReturn,
6
+ } from '../types';
7
+ import { DEFAULT_BIRTHDAY_VALUE } from '../constants';
8
+ import {
9
+ getDaysInMonth,
10
+ clampDay,
11
+ isValidBirthday,
12
+ normalizeBirthday,
13
+ } from '../utils/dateUtils';
14
+
15
+ /**
16
+ * Hook for managing birthday picker state
17
+ *
18
+ * @param options - Configuration options
19
+ * @returns State and handlers for birthday picker
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const { value, setMonth, setDay, daysInMonth } = useBirthdayPicker({
24
+ * initialValue: { month: 6, day: 15 },
25
+ * });
26
+ * ```
27
+ */
28
+ export function useBirthdayPicker(
29
+ options: UseBirthdayPickerOptions = {}
30
+ ): UseBirthdayPickerReturn {
31
+ const { initialValue = DEFAULT_BIRTHDAY_VALUE, allowLeapDay = true } =
32
+ options;
33
+
34
+ // Normalize the initial value
35
+ const normalizedInitial = useMemo(
36
+ () => normalizeBirthday(initialValue, allowLeapDay),
37
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
+ [] // Only compute once on mount
39
+ );
40
+
41
+ const [value, setValueInternal] = useState<BirthdayValue>(normalizedInitial);
42
+
43
+ /**
44
+ * Set the month, clamping the day if necessary
45
+ */
46
+ const setMonth = useCallback(
47
+ (month: number) => {
48
+ setValueInternal((prev) => {
49
+ const clampedMonth = Math.max(1, Math.min(month, 12));
50
+ const clampedDay = clampDay(prev.day, clampedMonth, allowLeapDay);
51
+ return { month: clampedMonth, day: clampedDay };
52
+ });
53
+ },
54
+ [allowLeapDay]
55
+ );
56
+
57
+ /**
58
+ * Set the day, clamping to valid range for current month
59
+ */
60
+ const setDay = useCallback(
61
+ (day: number) => {
62
+ setValueInternal((prev) => {
63
+ const clampedDay = clampDay(day, prev.month, allowLeapDay);
64
+ return { ...prev, day: clampedDay };
65
+ });
66
+ },
67
+ [allowLeapDay]
68
+ );
69
+
70
+ /**
71
+ * Set the entire value, normalizing both month and day
72
+ */
73
+ const setValue = useCallback(
74
+ (newValue: BirthdayValue) => {
75
+ setValueInternal(normalizeBirthday(newValue, allowLeapDay));
76
+ },
77
+ [allowLeapDay]
78
+ );
79
+
80
+ /**
81
+ * Number of days in the currently selected month
82
+ */
83
+ const daysInMonth = useMemo(
84
+ () => getDaysInMonth(value.month, allowLeapDay),
85
+ [value.month, allowLeapDay]
86
+ );
87
+
88
+ /**
89
+ * Whether the current value is valid
90
+ */
91
+ const isValid = useMemo(
92
+ () => isValidBirthday(value, allowLeapDay),
93
+ [value, allowLeapDay]
94
+ );
95
+
96
+ return {
97
+ value,
98
+ setMonth,
99
+ setDay,
100
+ setValue,
101
+ daysInMonth,
102
+ isValid,
103
+ };
104
+ }
105
+
106
+ export default useBirthdayPicker;
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ // Main components
2
+ export { BirthdayPicker } from './BirthdayPicker';
3
+ export { BirthdayPickerModal } from './BirthdayPickerModal';
4
+
5
+ // Hook
6
+ export { useBirthdayPicker } from './hooks/useBirthdayPicker';
7
+
8
+ // Types
9
+ export type {
10
+ BirthdayValue,
11
+ MonthFormat,
12
+ BirthdayPickerProps,
13
+ BirthdayPickerModalProps,
14
+ UseBirthdayPickerOptions,
15
+ UseBirthdayPickerReturn,
16
+ } from './types';
17
+
18
+ // Utilities (for advanced usage)
19
+ export {
20
+ getDaysInMonth,
21
+ clampDay,
22
+ isValidBirthday,
23
+ getDaysArray,
24
+ getMonthsArray,
25
+ normalizeBirthday,
26
+ } from './utils/dateUtils';
27
+
28
+ export { getMonthNames, formatMonth, formatDay } from './utils/localeUtils';
29
+
30
+ // Default export
31
+ export { BirthdayPicker as default } from './BirthdayPicker';
package/src/types.ts ADDED
@@ -0,0 +1,189 @@
1
+ import type { ViewStyle } from 'react-native';
2
+
3
+ /**
4
+ * Represents a month-day birthday value without year.
5
+ * Month is 1-indexed (1 = January, 12 = December)
6
+ * Day is 1-indexed (1-31 depending on month)
7
+ */
8
+ export type BirthdayValue = {
9
+ month: number; // 1-12
10
+ day: number; // 1-31
11
+ };
12
+
13
+ /**
14
+ * Format for displaying month names
15
+ */
16
+ export type MonthFormat = 'long' | 'short' | 'numeric';
17
+
18
+ /**
19
+ * Props for the BirthdayPicker component
20
+ */
21
+ export interface BirthdayPickerProps {
22
+ /**
23
+ * Current value (controlled mode)
24
+ */
25
+ value?: BirthdayValue;
26
+
27
+ /**
28
+ * Default value (uncontrolled mode)
29
+ */
30
+ defaultValue?: BirthdayValue;
31
+
32
+ /**
33
+ * Called when value changes
34
+ */
35
+ onChange?: (value: BirthdayValue) => void;
36
+
37
+ /**
38
+ * BCP 47 locale string for month name formatting
39
+ * @default 'en-US'
40
+ */
41
+ locale?: string;
42
+
43
+ /**
44
+ * How to format month names
45
+ * @default 'long'
46
+ */
47
+ monthFormat?: MonthFormat;
48
+
49
+ /**
50
+ * Whether to allow Feb 29 as a valid birthday
51
+ * @default true
52
+ */
53
+ allowLeapDay?: boolean;
54
+
55
+ /**
56
+ * Disable interaction
57
+ * @default false
58
+ */
59
+ disabled?: boolean;
60
+
61
+ /**
62
+ * Test ID for testing
63
+ */
64
+ testID?: string;
65
+
66
+ /**
67
+ * Container style
68
+ */
69
+ style?: ViewStyle;
70
+
71
+ /**
72
+ * Height of each item in the wheel
73
+ * @default 40
74
+ */
75
+ itemHeight?: number;
76
+
77
+ /**
78
+ * Number of visible items in each wheel (must be odd)
79
+ * @default 5
80
+ */
81
+ visibleItems?: number;
82
+
83
+ /**
84
+ * Accessibility label for month picker
85
+ * @default 'Month picker'
86
+ */
87
+ monthAccessibilityLabel?: string;
88
+
89
+ /**
90
+ * Accessibility label for day picker
91
+ * @default 'Day picker'
92
+ */
93
+ dayAccessibilityLabel?: string;
94
+ }
95
+
96
+ /**
97
+ * Props for the BirthdayPickerModal component
98
+ */
99
+ export interface BirthdayPickerModalProps extends BirthdayPickerProps {
100
+ /**
101
+ * Whether the modal is visible
102
+ */
103
+ visible: boolean;
104
+
105
+ /**
106
+ * Called when user confirms selection
107
+ */
108
+ onConfirm: (value: BirthdayValue) => void;
109
+
110
+ /**
111
+ * Called when user cancels
112
+ */
113
+ onCancel: () => void;
114
+
115
+ /**
116
+ * Modal title
117
+ * @default 'Select Birthday'
118
+ */
119
+ title?: string;
120
+
121
+ /**
122
+ * Confirm button text
123
+ * @default 'Confirm'
124
+ */
125
+ confirmText?: string;
126
+
127
+ /**
128
+ * Cancel button text
129
+ * @default 'Cancel'
130
+ */
131
+ cancelText?: string;
132
+
133
+ /**
134
+ * Animation type for modal presentation
135
+ * @default 'slide'
136
+ */
137
+ animationType?: 'slide' | 'fade' | 'none';
138
+ }
139
+
140
+ /**
141
+ * Options for useBirthdayPicker hook
142
+ */
143
+ export interface UseBirthdayPickerOptions {
144
+ /**
145
+ * Initial value
146
+ */
147
+ initialValue?: BirthdayValue;
148
+
149
+ /**
150
+ * Whether to allow Feb 29
151
+ * @default true
152
+ */
153
+ allowLeapDay?: boolean;
154
+ }
155
+
156
+ /**
157
+ * Return type of useBirthdayPicker hook
158
+ */
159
+ export interface UseBirthdayPickerReturn {
160
+ /**
161
+ * Current birthday value
162
+ */
163
+ value: BirthdayValue;
164
+
165
+ /**
166
+ * Set the month (will clamp day if needed)
167
+ */
168
+ setMonth: (month: number) => void;
169
+
170
+ /**
171
+ * Set the day
172
+ */
173
+ setDay: (day: number) => void;
174
+
175
+ /**
176
+ * Set the entire value
177
+ */
178
+ setValue: (value: BirthdayValue) => void;
179
+
180
+ /**
181
+ * Number of days in the currently selected month
182
+ */
183
+ daysInMonth: number;
184
+
185
+ /**
186
+ * Whether the current value is valid
187
+ */
188
+ isValid: boolean;
189
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ DAYS_IN_MONTH,
3
+ FEBRUARY,
4
+ LEAP_DAY,
5
+ MONTHS_IN_YEAR,
6
+ } from '../constants';
7
+ import type { BirthdayValue } from '../types';
8
+
9
+ /**
10
+ * Get the number of days in a given month
11
+ * @param month - Month (1-12)
12
+ * @param allowLeapDay - Whether to allow Feb 29
13
+ * @returns Number of days in the month
14
+ */
15
+ export function getDaysInMonth(month: number, allowLeapDay = true): number {
16
+ if (month < 1 || month > MONTHS_IN_YEAR) {
17
+ throw new Error(`Invalid month: ${month}. Must be between 1 and 12.`);
18
+ }
19
+
20
+ if (month === FEBRUARY && allowLeapDay) {
21
+ return LEAP_DAY;
22
+ }
23
+
24
+ return DAYS_IN_MONTH[month];
25
+ }
26
+
27
+ /**
28
+ * Clamp a day value to be valid for a given month
29
+ * @param day - Day to clamp
30
+ * @param month - Month (1-12)
31
+ * @param allowLeapDay - Whether to allow Feb 29
32
+ * @returns Clamped day value
33
+ */
34
+ export function clampDay(
35
+ day: number,
36
+ month: number,
37
+ allowLeapDay = true
38
+ ): number {
39
+ const maxDays = getDaysInMonth(month, allowLeapDay);
40
+ return Math.max(1, Math.min(day, maxDays));
41
+ }
42
+
43
+ /**
44
+ * Check if a birthday value is valid
45
+ * @param value - Birthday value to validate
46
+ * @param allowLeapDay - Whether to allow Feb 29
47
+ * @returns Whether the value is valid
48
+ */
49
+ export function isValidBirthday(
50
+ value: BirthdayValue,
51
+ allowLeapDay = true
52
+ ): boolean {
53
+ const { month, day } = value;
54
+
55
+ // Check month range
56
+ if (month < 1 || month > MONTHS_IN_YEAR) {
57
+ return false;
58
+ }
59
+
60
+ // Check day range
61
+ const maxDays = getDaysInMonth(month, allowLeapDay);
62
+ if (day < 1 || day > maxDays) {
63
+ return false;
64
+ }
65
+
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * Generate an array of day numbers for a given month
71
+ * @param month - Month (1-12)
72
+ * @param allowLeapDay - Whether to allow Feb 29
73
+ * @returns Array of day numbers [1, 2, 3, ..., n]
74
+ */
75
+ export function getDaysArray(month: number, allowLeapDay = true): number[] {
76
+ const maxDays = getDaysInMonth(month, allowLeapDay);
77
+ return Array.from({ length: maxDays }, (_, i) => i + 1);
78
+ }
79
+
80
+ /**
81
+ * Generate an array of month numbers
82
+ * @returns Array of month numbers [1, 2, 3, ..., 12]
83
+ */
84
+ export function getMonthsArray(): number[] {
85
+ return Array.from({ length: MONTHS_IN_YEAR }, (_, i) => i + 1);
86
+ }
87
+
88
+ /**
89
+ * Normalize a birthday value to ensure it's valid
90
+ * @param value - Birthday value to normalize
91
+ * @param allowLeapDay - Whether to allow Feb 29
92
+ * @returns Normalized birthday value
93
+ */
94
+ export function normalizeBirthday(
95
+ value: BirthdayValue,
96
+ allowLeapDay = true
97
+ ): BirthdayValue {
98
+ const month = Math.max(1, Math.min(value.month, MONTHS_IN_YEAR));
99
+ const day = clampDay(value.day, month, allowLeapDay);
100
+ return { month, day };
101
+ }