1+ /**
2+ * Utility functions for month selection and date handling
3+ * Focused, tested utilities with proper error handling and validation
4+ */
5+
6+ import { CopilotUsageData } from '@/lib/utils' ;
7+ import { MonthOption , MonthCoverage , MonthIdentifier } from '@/types/month' ;
8+
9+ /**
10+ * Validates if a month string is in the correct YYYY-MM format
11+ */
12+ export function isValidMonthFormat ( monthStr : string ) : boolean {
13+ const pattern = / ^ \d { 4 } - \d { 2 } $ / ;
14+ if ( ! pattern . test ( monthStr ) ) return false ;
15+
16+ const [ year , month ] = monthStr . split ( '-' ) . map ( Number ) ;
17+ return year >= 2000 && year <= 3000 && month >= 1 && month <= 12 ;
18+ }
19+
20+ /**
21+ * Parses a month string into year and month numbers
22+ * @throws Error if format is invalid
23+ */
24+ export function parseMonthString ( monthStr : string ) : MonthIdentifier {
25+ if ( ! isValidMonthFormat ( monthStr ) ) {
26+ throw new Error ( `Invalid month format: ${ monthStr } . Expected YYYY-MM format.` ) ;
27+ }
28+
29+ const [ year , month ] = monthStr . split ( '-' ) . map ( Number ) ;
30+ return { year, month } ;
31+ }
32+
33+ /**
34+ * Formats a MonthIdentifier into YYYY-MM string
35+ */
36+ export function formatMonthString ( identifier : MonthIdentifier ) : string {
37+ const { year, month } = identifier ;
38+ return `${ year } -${ String ( month ) . padStart ( 2 , '0' ) } ` ;
39+ }
40+
41+ /**
42+ * Gets the current month identifier
43+ */
44+ export function getCurrentMonth ( ) : MonthIdentifier {
45+ const now = new Date ( ) ;
46+ return {
47+ year : now . getFullYear ( ) ,
48+ month : now . getMonth ( ) + 1 // JavaScript months are 0-indexed
49+ } ;
50+ }
51+
52+ /**
53+ * Gets the previous month identifier
54+ */
55+ export function getPreviousMonth ( from ?: MonthIdentifier ) : MonthIdentifier {
56+ const base = from || getCurrentMonth ( ) ;
57+ const { year, month } = base ;
58+
59+ if ( month === 1 ) {
60+ return { year : year - 1 , month : 12 } ;
61+ }
62+ return { year, month : month - 1 } ;
63+ }
64+
65+ /**
66+ * Creates a human-readable label for a month
67+ */
68+ export function createMonthLabel ( identifier : MonthIdentifier ) : string {
69+ const { year, month } = identifier ;
70+ const date = new Date ( year , month - 1 , 1 ) ;
71+ return date . toLocaleDateString ( 'en-US' , { month : 'long' , year : 'numeric' } ) ;
72+ }
73+
74+ /**
75+ * Extracts unique month identifiers from usage data
76+ */
77+ export function extractMonthsFromData ( data : CopilotUsageData [ ] ) : MonthIdentifier [ ] {
78+ if ( ! data || data . length === 0 ) return [ ] ;
79+
80+ const monthsSet = new Set < string > ( ) ;
81+
82+ for ( const item of data ) {
83+ if ( ! item . timestamp || ! ( item . timestamp instanceof Date ) ) continue ;
84+
85+ const identifier : MonthIdentifier = {
86+ year : item . timestamp . getFullYear ( ) ,
87+ month : item . timestamp . getMonth ( ) + 1
88+ } ;
89+
90+ monthsSet . add ( formatMonthString ( identifier ) ) ;
91+ }
92+
93+ return Array . from ( monthsSet )
94+ . sort ( )
95+ . reverse ( ) // Most recent first
96+ . map ( parseMonthString ) ;
97+ }
98+
99+ /**
100+ * Gets available month options for the month selector
101+ * Returns all months that have data in the export
102+ */
103+ export function getAvailableMonths ( data : CopilotUsageData [ ] ) : MonthOption [ ] {
104+ if ( ! data || data . length === 0 ) return [ ] ;
105+
106+ const monthsWithData = extractMonthsFromData ( data ) ;
107+ const currentMonth = getCurrentMonth ( ) ;
108+ const currentMonthStr = formatMonthString ( currentMonth ) ;
109+
110+ // Return all months with data (already sorted newest to oldest)
111+ return monthsWithData . map ( identifier => ( {
112+ value : formatMonthString ( identifier ) ,
113+ label : createMonthLabel ( identifier ) ,
114+ isCurrentMonth : formatMonthString ( identifier ) === currentMonthStr
115+ } ) ) ;
116+ }
117+
118+ /**
119+ * Filters usage data by the specified month
120+ */
121+ export function filterDataByMonth ( data : CopilotUsageData [ ] , selectedMonth : string ) : CopilotUsageData [ ] {
122+ if ( ! data || data . length === 0 ) return [ ] ;
123+ if ( ! selectedMonth ) return data ;
124+
125+ const monthIdentifier = parseMonthString ( selectedMonth ) ;
126+ const { year, month } = monthIdentifier ;
127+
128+ return data . filter ( item => {
129+ if ( ! item . timestamp || ! ( item . timestamp instanceof Date ) ) return false ;
130+
131+ return item . timestamp . getFullYear ( ) === year &&
132+ item . timestamp . getMonth ( ) === month - 1 ;
133+ } ) ;
134+ }
135+
136+ /**
137+ * Calculates the number of days in a given month
138+ */
139+ export function getDaysInMonth ( identifier : MonthIdentifier ) : number {
140+ const { year, month } = identifier ;
141+ return new Date ( year , month , 0 ) . getDate ( ) ;
142+ }
143+
144+ /**
145+ * Gets unique days with data for a specific month
146+ */
147+ export function getUniqueDaysWithData ( data : CopilotUsageData [ ] , monthIdentifier : MonthIdentifier ) : Set < number > {
148+ const { year, month } = monthIdentifier ;
149+ const daysSet = new Set < number > ( ) ;
150+
151+ for ( const item of data ) {
152+ if ( ! item . timestamp || ! ( item . timestamp instanceof Date ) ) continue ;
153+
154+ if ( item . timestamp . getFullYear ( ) === year &&
155+ item . timestamp . getMonth ( ) === month - 1 ) {
156+ daysSet . add ( item . timestamp . getDate ( ) ) ;
157+ }
158+ }
159+
160+ return daysSet ;
161+ }
162+
163+ /**
164+ * Calculates month coverage information
165+ */
166+ export function getMonthCoverage ( data : CopilotUsageData [ ] , selectedMonth : string ) : MonthCoverage {
167+ if ( ! data || data . length === 0 || ! selectedMonth ) {
168+ return { daysWithData : 0 , totalDays : 0 , isCurrentMonth : false } ;
169+ }
170+
171+ const monthIdentifier = parseMonthString ( selectedMonth ) ;
172+ const currentMonth = getCurrentMonth ( ) ;
173+ const isCurrentMonth = formatMonthString ( monthIdentifier ) === formatMonthString ( currentMonth ) ;
174+
175+ const totalDays = getDaysInMonth ( monthIdentifier ) ;
176+ const uniqueDays = getUniqueDaysWithData ( data , monthIdentifier ) ;
177+
178+ return {
179+ daysWithData : uniqueDays . size ,
180+ totalDays,
181+ isCurrentMonth
182+ } ;
183+ }
0 commit comments