Skip to content

Commit d094b4c

Browse files
committed
Extract month filtering logic to DateUtils.getFilteredMonthItems
1 parent 9378591 commit d094b4c

3 files changed

Lines changed: 40 additions & 64 deletions

File tree

src/components/DatePicker/CalendarPicker/MonthPickerModal.tsx

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {endOfMonth, startOfMonth} from 'date-fns';
21
import React, {useEffect, useMemo, useState} from 'react';
32
import {Keyboard} from 'react-native';
43
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -40,28 +39,7 @@ function MonthPickerModal({isVisible, currentMonth = new Date().getMonth(), curr
4039
const [searchText, setSearchText] = useState('');
4140
const monthNames = DateUtils.getMonthNames();
4241

43-
const allMonths = useMemo(() => {
44-
const minMonthStart = minDate ? startOfMonth(new Date(minDate)) : undefined;
45-
const maxMonthEnd = maxDate ? endOfMonth(new Date(maxDate)) : undefined;
46-
47-
return monthNames
48-
.map((month, index) => {
49-
const monthStart = startOfMonth(new Date(currentYear, index));
50-
const monthEnd = endOfMonth(new Date(currentYear, index));
51-
const isBeforeMin = minMonthStart ? monthEnd < minMonthStart : false;
52-
const isAfterMax = maxMonthEnd ? monthStart > maxMonthEnd : false;
53-
if (isBeforeMin || isAfterMax) {
54-
return null;
55-
}
56-
return {
57-
text: month.charAt(0).toUpperCase() + month.slice(1),
58-
value: index,
59-
keyForList: index.toString(),
60-
isSelected: index === currentMonth,
61-
};
62-
})
63-
.filter((item): item is NonNullable<typeof item> => item !== null);
64-
}, [monthNames, currentMonth, currentYear, minDate, maxDate]);
42+
const allMonths = useMemo(() => DateUtils.getFilteredMonthItems(monthNames, currentYear, currentMonth, minDate, maxDate), [monthNames, currentMonth, currentYear, minDate, maxDate]);
6543

6644
const {data, headerMessage} = useMemo(() => {
6745
const filteredMonths = searchText === '' ? allMonths : allMonths.filter((month) => month.text.toLowerCase().includes(searchText.toLowerCase()));

src/libs/DateUtils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
parse,
2626
set,
2727
startOfDay,
28+
startOfMonth,
2829
startOfWeek,
2930
subDays,
3031
subMilliseconds,
@@ -304,6 +305,32 @@ function getMonthNames(): string[] {
304305
return monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT));
305306
}
306307

308+
/**
309+
* Filters month names by min/max date boundaries and returns list items for SelectionList.
310+
*/
311+
function getFilteredMonthItems(monthNames: string[], currentYear: number, currentMonth: number, minDate?: Date, maxDate?: Date) {
312+
const minMonthStart = minDate ? startOfMonth(new Date(minDate)) : undefined;
313+
const maxMonthEnd = maxDate ? endOfMonth(new Date(maxDate)) : undefined;
314+
315+
return monthNames
316+
.map((month, index) => {
317+
const monthStart = startOfMonth(new Date(currentYear, index));
318+
const monthEnd = endOfMonth(new Date(currentYear, index));
319+
const isBeforeMin = minMonthStart ? monthEnd < minMonthStart : false;
320+
const isAfterMax = maxMonthEnd ? monthStart > maxMonthEnd : false;
321+
if (isBeforeMin || isAfterMax) {
322+
return null;
323+
}
324+
return {
325+
text: month.charAt(0).toUpperCase() + month.slice(1),
326+
value: index,
327+
keyForList: index.toString(),
328+
isSelected: index === currentMonth,
329+
};
330+
})
331+
.filter((item): item is NonNullable<typeof item> => item !== null);
332+
}
333+
307334
/**
308335
* @returns [Monday, Tuesday, Wednesday, ...]
309336
*/
@@ -1093,6 +1120,7 @@ const DateUtils = {
10931120
isTomorrow,
10941121
isYesterday,
10951122
getMonthNames,
1123+
getFilteredMonthItems,
10961124
getDaysOfWeek,
10971125
formatWithUTCTimeZone,
10981126
getWeekEndsOn,

tests/unit/CalendarPickerTest.tsx

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type * as ReactNavigationNative from '@react-navigation/native';
22
import {fireEvent, render, screen, userEvent, within} from '@testing-library/react-native';
3-
import {addMonths, addYears, endOfMonth, startOfMonth, subMonths, subYears} from 'date-fns';
3+
import {addMonths, addYears, subMonths, subYears} from 'date-fns';
44
import CalendarPicker from '@components/DatePicker/CalendarPicker';
55
import DateUtils from '@libs/DateUtils';
66

@@ -425,58 +425,28 @@ describe('CalendarPicker', () => {
425425
});
426426

427427
test('month picker filtering should exclude months before minDate', () => {
428-
const currentYear = 2023;
429-
const minDate = new Date('2023-06-01');
430-
const maxDate = new Date('2030-12-31');
431-
432-
const filteredMonths = monthNames
433-
.map((month, index) => {
434-
const monthStart = startOfMonth(new Date(currentYear, index));
435-
const monthEnd = endOfMonth(new Date(currentYear, index));
436-
const isBeforeMin = monthEnd < startOfMonth(new Date(minDate));
437-
const isAfterMax = monthStart > endOfMonth(new Date(maxDate));
438-
if (isBeforeMin || isAfterMax) {
439-
return null;
440-
}
441-
return {text: month, value: index};
442-
})
443-
.filter(Boolean);
428+
const filteredMonths = DateUtils.getFilteredMonthItems(monthNames, 2023, 6, new Date('2023-06-01'), new Date('2030-12-31'));
444429

445430
// Months before June (index 5) should be excluded
446-
expect(filteredMonths.find((m) => m?.value === 0)).toBeUndefined();
447-
expect(filteredMonths.find((m) => m?.value === 4)).toBeUndefined();
431+
expect(filteredMonths.find((m) => m.value === 0)).toBeUndefined();
432+
expect(filteredMonths.find((m) => m.value === 4)).toBeUndefined();
448433

449434
// June and later months should be included
450-
expect(filteredMonths.find((m) => m?.value === 5)).toBeTruthy();
451-
expect(filteredMonths.find((m) => m?.value === 11)).toBeTruthy();
435+
expect(filteredMonths.find((m) => m.value === 5)).toBeTruthy();
436+
expect(filteredMonths.find((m) => m.value === 11)).toBeTruthy();
452437
expect(filteredMonths).toHaveLength(7);
453438
});
454439

455440
test('month picker filtering should exclude months after maxDate', () => {
456-
const currentYear = 2023;
457-
const minDate = new Date('2020-01-01');
458-
const maxDate = new Date('2023-09-30');
459-
460-
const filteredMonths = monthNames
461-
.map((month, index) => {
462-
const monthStart = startOfMonth(new Date(currentYear, index));
463-
const monthEnd = endOfMonth(new Date(currentYear, index));
464-
const isBeforeMin = monthEnd < startOfMonth(new Date(minDate));
465-
const isAfterMax = monthStart > endOfMonth(new Date(maxDate));
466-
if (isBeforeMin || isAfterMax) {
467-
return null;
468-
}
469-
return {text: month, value: index};
470-
})
471-
.filter(Boolean);
441+
const filteredMonths = DateUtils.getFilteredMonthItems(monthNames, 2023, 0, new Date('2020-01-01'), new Date('2023-09-30'));
472442

473443
// Months after September (index 8) should be excluded
474-
expect(filteredMonths.find((m) => m?.value === 10)).toBeUndefined();
475-
expect(filteredMonths.find((m) => m?.value === 11)).toBeUndefined();
444+
expect(filteredMonths.find((m) => m.value === 10)).toBeUndefined();
445+
expect(filteredMonths.find((m) => m.value === 11)).toBeUndefined();
476446

477447
// September and earlier months should be included
478-
expect(filteredMonths.find((m) => m?.value === 8)).toBeTruthy();
479-
expect(filteredMonths.find((m) => m?.value === 0)).toBeTruthy();
448+
expect(filteredMonths.find((m) => m.value === 8)).toBeTruthy();
449+
expect(filteredMonths.find((m) => m.value === 0)).toBeTruthy();
480450
expect(filteredMonths).toHaveLength(9);
481451
});
482452
});

0 commit comments

Comments
 (0)