PluginProbe
The Innovative Form Builder – IvyForms / 0.9.1
The Innovative Form Builder – IvyForms v0.9.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / composables / useDateField.ts

useDateField.ts in The Innovative Form Builder – IvyForms 0.9.1, at frontend/src/composables/useDateField.ts

299 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 export function useDateField() {
2 function parseDateString(
3 dateStr: string,
4 format?: string,
5 ): { day: string; month: string; year: string } {
6 if (!dateStr) {
7 return { day: '', month: '', year: '' }
8 }
9
10 // Try to parse based on separators
11 const separators = ['-', '/', '.']
12 let parts: string[] = []
13
14 for (const sep of separators) {
15 if (dateStr.includes(sep)) {
16 parts = dateStr.split(sep)
17 break
18 }
19 }
20
21 if (parts.length !== 3) {
22 return { day: '', month: '', year: '' }
23 }
24
25 // Determine format (default to MM/DD/YYYY if not provided)
26 const dateFormat = (format || 'MM/DD/YYYY').toUpperCase()
27
28 let day = ''
29 let month = ''
30 let year = ''
31
32 const formatParts = dateFormat.split(/[/.-]/)
33 formatParts.forEach((part, index) => {
34 const upperPart = part.toUpperCase()
35 if (upperPart === 'DD' || upperPart === 'D') day = parts[index]
36 if (upperPart === 'MM' || upperPart === 'M') month = parts[index]
37 if (upperPart === 'YYYY' || upperPart === 'YY') year = parts[index]
38 })
39
40 return { day, month, year }
41 }
42
43 function formatDateString(
44 day: string,
45 month: string,
46 year: string,
47 format: string = 'MM/DD/YYYY',
48 ): string {
49 if (!day || !month || !year) return ''
50
51 const separator = format.includes('/') ? '/' : format.includes('-') ? '-' : '.'
52
53 const formatParts = format.split(/[/.-]/)
54 const result = formatParts.map((part) => {
55 const upperPart = part.toUpperCase()
56 if (upperPart === 'DD' || upperPart === 'D') {
57 return upperPart === 'DD' ? day.padStart(2, '0') : day
58 }
59 if (upperPart === 'MM' || upperPart === 'M') {
60 return upperPart === 'MM' ? month.padStart(2, '0') : month
61 }
62 if (upperPart === 'YYYY') return year
63 if (upperPart === 'YY') return year.slice(-2)
64 return part
65 })
66
67 return result.join(separator)
68 }
69
70 function isValidDate(
71 day: string | number,
72 month: string | number,
73 year: string | number,
74 ): boolean {
75 const d = typeof day === 'string' ? parseInt(day, 10) : day
76 const m = typeof month === 'string' ? parseInt(month, 10) : month
77 const y = typeof year === 'string' ? parseInt(year, 10) : year
78
79 if (isNaN(d) || isNaN(m) || isNaN(y)) return false
80 if (m < 1 || m > 12) return false
81 if (d < 1 || d > 31) return false
82 if (y < 1000 || y > 9999) return false
83
84 // Check day validity for month
85 const daysInMonth = new Date(y, m, 0).getDate()
86 return d <= daysInMonth
87 }
88
89 function getDaysArray(): Array<{ label: string; value: string }> {
90 return Array.from({ length: 31 }, (_, i) => {
91 const day = (i + 1).toString().padStart(2, '0')
92 return { label: day, value: day }
93 })
94 }
95
96 function getMonthsArray(): Array<{ label: string; value: string }> {
97 return Array.from({ length: 12 }, (_, i) => {
98 const month = (i + 1).toString().padStart(2, '0')
99 return { label: month, value: month }
100 })
101 }
102
103 function getYearsArray(
104 minYear?: number,
105 maxYear?: number,
106 ): Array<{ label: string; value: string }> {
107 const currentYear = new Date().getFullYear()
108 const min = minYear || currentYear - 100
109 const max = maxYear || currentYear + 100
110
111 const years: Array<{ label: string; value: string }> = []
112 for (let year = min; year <= max; year++) {
113 years.push({ label: year.toString(), value: year.toString() })
114 }
115 return years
116 }
117
118 function toISOFormat(dateStr: string, format?: string): string {
119 const parsed = parseDateString(dateStr, format)
120 if (!parsed.day || !parsed.month || !parsed.year) return ''
121 return `${parsed.year}-${parsed.month.padStart(2, '0')}-${parsed.day.padStart(2, '0')}`
122 }
123
124 function fromISOFormat(isoDate: string, format: string = 'MM/DD/YYYY'): string {
125 if (!isoDate) return ''
126 const [year, month, day] = isoDate.split('-')
127 return formatDateString(day, month, year, format)
128 }
129
130 /**
131 * Validate date format based on date format pattern
132 * Returns true if date matches the expected format
133 */
134 function isValidDateFormat(dateStr: string, dateFormat: string): boolean {
135 if (!dateStr || typeof dateStr !== 'string') {
136 return false
137 }
138
139 const trimmed = dateStr.trim()
140
141 if (dateFormat === 'MM/DD/YYYY' || dateFormat === 'M/D/YYYY') {
142 return /^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/\d{4}$/.test(trimmed)
143 } else if (dateFormat === 'DD/MM/YYYY' || dateFormat === 'D/M/YYYY') {
144 return /^(0?[1-9]|[12]\d|3[01])\/(0?[1-9]|1[0-2])\/\d{4}$/.test(trimmed)
145 } else if (dateFormat === 'YYYY-MM-DD' || dateFormat === 'YYYY-M-D') {
146 return /^\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[12]\d|3[01])$/.test(trimmed)
147 } else if (dateFormat === 'DD.MM.YYYY' || dateFormat === 'D.M.YYYY') {
148 return /^(0?[1-9]|[12]\d|3[01])\.(0?[1-9]|1[0-2])\.\d{4}$/.test(trimmed)
149 } else {
150 // Default to MM/DD/YYYY
151 return /^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/\d{4}$/.test(trimmed)
152 }
153 }
154
155 /**
156 * Parse date string to comparable object
157 * Handles multiple date formats and returns standardized object for comparison
158 * Returns null if parsing fails
159 */
160 function parseDateToObject(
161 dateStr: string,
162 dateFormat: string,
163 ): { year: number; month: number; day: number } | null {
164 if (!dateStr || typeof dateStr !== 'string') {
165 return null
166 }
167
168 const trimmed = dateStr.trim()
169 let year: number, month: number, day: number
170
171 if (dateFormat === 'MM/DD/YYYY' || dateFormat === 'M/D/YYYY') {
172 const match = trimmed.match(/^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/(\d{4})$/)
173 if (!match) return null
174 month = parseInt(match[1], 10)
175 day = parseInt(match[2], 10)
176 year = parseInt(match[3], 10)
177 } else if (dateFormat === 'DD/MM/YYYY' || dateFormat === 'D/M/YYYY') {
178 const match = trimmed.match(/^(0?[1-9]|[12]\d|3[01])\/(0?[1-9]|1[0-2])\/(\d{4})$/)
179 if (!match) return null
180 day = parseInt(match[1], 10)
181 month = parseInt(match[2], 10)
182 year = parseInt(match[3], 10)
183 } else if (dateFormat === 'YYYY-MM-DD' || dateFormat === 'YYYY-M-D') {
184 const match = trimmed.match(/^(\d{4})-(0?[1-9]|1[0-2])-(0?[1-9]|[12]\d|3[01])$/)
185 if (!match) return null
186 year = parseInt(match[1], 10)
187 month = parseInt(match[2], 10)
188 day = parseInt(match[3], 10)
189 } else if (dateFormat === 'DD.MM.YYYY' || dateFormat === 'D.M.YYYY') {
190 const match = trimmed.match(/^(0?[1-9]|[12]\d|3[01])\.(0?[1-9]|1[0-2])\.(\d{4})$/)
191 if (!match) return null
192 day = parseInt(match[1], 10)
193 month = parseInt(match[2], 10)
194 year = parseInt(match[3], 10)
195 } else {
196 // Default to MM/DD/YYYY
197 const match = trimmed.match(/^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/(\d{4})$/)
198 if (!match) return null
199 month = parseInt(match[1], 10)
200 day = parseInt(match[2], 10)
201 year = parseInt(match[3], 10)
202 }
203
204 // Validate ranges
205 if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1900 || year > 2100) {
206 return null
207 }
208
209 return { year, month, day }
210 }
211
212 /**
213 * Compare two date objects for range validation
214 * Returns: negative if a < b, 0 if equal, positive if a > b
215 */
216 function compareDates(
217 a: { year: number; month: number; day: number } | null,
218 b: { year: number; month: number; day: number } | null,
219 ): number {
220 if (!a || !b) return 0
221
222 if (a.year !== b.year) return a.year - b.year
223 if (a.month !== b.month) return a.month - b.month
224 return a.day - b.day
225 }
226
227 /**
228 * Validate date is within min/max range
229 * Returns: { isInRange: boolean, error?: string }
230 */
231 function isDateInValidRange(
232 dateStr: string,
233 dateFormat: string,
234 minDateValue?: string | null,
235 maxDateValue?: string | null,
236 ): { isInRange: boolean; error?: string } {
237 // Empty values are allowed (checked by required rule)
238 if (!dateStr) {
239 return { isInRange: true }
240 }
241
242 const currentDate = parseDateToObject(dateStr, dateFormat)
243 if (!currentDate) {
244 return { isInRange: false, error: 'Invalid date format' }
245 }
246
247 const hasMin = minDateValue !== null && minDateValue !== undefined && minDateValue !== ''
248 const hasMax = maxDateValue !== null && maxDateValue !== undefined && maxDateValue !== ''
249
250 if (!hasMin && !hasMax) {
251 return { isInRange: true }
252 }
253
254 // Parse min date
255 let minDate: { year: number; month: number; day: number } | null = null
256 if (hasMin) {
257 minDate = parseDateToObject(minDateValue!, dateFormat)
258 if (!minDate) {
259 return { isInRange: false, error: `Malformed minimum date value: ${minDateValue}` }
260 }
261 }
262
263 // Parse max date
264 let maxDate: { year: number; month: number; day: number } | null = null
265 if (hasMax) {
266 maxDate = parseDateToObject(maxDateValue!, dateFormat)
267 if (!maxDate) {
268 return { isInRange: false, error: `Malformed maximum date value: ${maxDateValue}` }
269 }
270 }
271
272 // Check range
273 if (minDate !== null && compareDates(currentDate, minDate) < 0) {
274 return { isInRange: false, error: `Date must be at or after ${minDateValue}` }
275 }
276
277 if (maxDate !== null && compareDates(currentDate, maxDate) > 0) {
278 return { isInRange: false, error: `Date must be at or before ${maxDateValue}` }
279 }
280
281 return { isInRange: true }
282 }
283
284 return {
285 parseDateString,
286 formatDateString,
287 isValidDate,
288 getDaysArray,
289 getMonthsArray,
290 getYearsArray,
291 toISOFormat,
292 fromISOFormat,
293 isValidDateFormat,
294 parseDateToObject,
295 compareDates,
296 isDateInValidRange,
297 }
298 }
299