@@ -312,6 +312,39 @@ function evaluateFormulaFunction(name: string, args: string[], context: Conditio
312312 }
313313 return Number . NaN ;
314314 }
315+ if ( normalizedName === "EDATE" ) {
316+ return addExcelSerialMonths (
317+ Number ( evaluateFormulaValue ( args [ 0 ] ?? "" , context ) ) ,
318+ Number ( evaluateFormulaValue ( args [ 1 ] ?? "0" , context ) ) ,
319+ ) ;
320+ }
321+ if ( normalizedName === "EOMONTH" ) {
322+ return endOfExcelSerialMonth (
323+ Number ( evaluateFormulaValue ( args [ 0 ] ?? "" , context ) ) ,
324+ Number ( evaluateFormulaValue ( args [ 1 ] ?? "0" , context ) ) ,
325+ ) ;
326+ }
327+ if ( normalizedName === "NETWORKDAYS" ) {
328+ return networkDaysFormulaNumber (
329+ Number ( evaluateFormulaValue ( args [ 0 ] ?? "" , context ) ) ,
330+ Number ( evaluateFormulaValue ( args [ 1 ] ?? "" , context ) ) ,
331+ formulaNumericArgs ( [ args [ 2 ] ?? "" ] , context ) ,
332+ ) ;
333+ }
334+ if ( normalizedName === "WORKDAY" ) {
335+ return workdayFormulaNumber (
336+ Number ( evaluateFormulaValue ( args [ 0 ] ?? "" , context ) ) ,
337+ Number ( evaluateFormulaValue ( args [ 1 ] ?? "" , context ) ) ,
338+ formulaNumericArgs ( [ args [ 2 ] ?? "" ] , context ) ,
339+ ) ;
340+ }
341+ if ( normalizedName === "DATEDIF" ) {
342+ return datedifFormulaNumber (
343+ Number ( evaluateFormulaValue ( args [ 0 ] ?? "" , context ) ) ,
344+ Number ( evaluateFormulaValue ( args [ 1 ] ?? "" , context ) ) ,
345+ asString ( evaluateFormulaValue ( args [ 2 ] ?? "\"D\"" , context ) ) ,
346+ ) ;
347+ }
315348 if ( normalizedName === "YEAR" || normalizedName === "MONTH" || normalizedName === "DAY" || normalizedName === "WEEKDAY" ) {
316349 return excelSerialDatePart ( Number ( evaluateFormulaValue ( args [ 0 ] ?? "" , context ) ) , normalizedName , Number ( evaluateFormulaValue ( args [ 1 ] ?? "1" , context ) ) ) ;
317350 }
@@ -879,9 +912,14 @@ function excelSerialDay(year: number, month: number, day: number): number {
879912 return Math . floor ( ( Date . UTC ( year , month - 1 , day ) - EXCEL_SERIAL_EPOCH_UTC ) / DAY_MS ) ;
880913}
881914
915+ function excelSerialUtcDate ( value : number ) : Date | null {
916+ if ( ! Number . isFinite ( value ) ) return null ;
917+ return new Date ( EXCEL_SERIAL_EPOCH_UTC + Math . floor ( value ) * DAY_MS ) ;
918+ }
919+
882920function excelSerialDatePart ( value : number , part : string , weekdayReturnType : number ) : number {
883- if ( ! Number . isFinite ( value ) ) return Number . NaN ;
884- const date = new Date ( EXCEL_SERIAL_EPOCH_UTC + Math . floor ( value ) * DAY_MS ) ;
921+ const date = excelSerialUtcDate ( value ) ;
922+ if ( ! date ) return Number . NaN ;
885923 if ( part === "YEAR" ) return date . getUTCFullYear ( ) ;
886924 if ( part === "MONTH" ) return date . getUTCMonth ( ) + 1 ;
887925 if ( part === "DAY" ) return date . getUTCDate ( ) ;
@@ -891,6 +929,78 @@ function excelSerialDatePart(value: number, part: string, weekdayReturnType: num
891929 return weekday + 1 ;
892930}
893931
932+ function addExcelSerialMonths ( value : number , months : number ) : number {
933+ const date = excelSerialUtcDate ( value ) ;
934+ if ( ! date || ! Number . isFinite ( months ) ) return Number . NaN ;
935+ const targetMonthIndex = date . getUTCMonth ( ) + Math . trunc ( months ) ;
936+ const targetYear = date . getUTCFullYear ( ) + Math . floor ( targetMonthIndex / 12 ) ;
937+ const targetMonth = modulo ( targetMonthIndex , 12 ) + 1 ;
938+ const targetDay = Math . min ( date . getUTCDate ( ) , daysInUtcMonth ( targetYear , targetMonth ) ) ;
939+ return excelSerialDay ( targetYear , targetMonth , targetDay ) ;
940+ }
941+
942+ function endOfExcelSerialMonth ( value : number , months : number ) : number {
943+ const date = excelSerialUtcDate ( value ) ;
944+ if ( ! date || ! Number . isFinite ( months ) ) return Number . NaN ;
945+ const targetMonthIndex = date . getUTCMonth ( ) + Math . trunc ( months ) ;
946+ const targetYear = date . getUTCFullYear ( ) + Math . floor ( targetMonthIndex / 12 ) ;
947+ const targetMonth = modulo ( targetMonthIndex , 12 ) + 1 ;
948+ return excelSerialDay ( targetYear , targetMonth , daysInUtcMonth ( targetYear , targetMonth ) ) ;
949+ }
950+
951+ function networkDaysFormulaNumber ( startValue : number , endValue : number , holidays : number [ ] ) : number {
952+ if ( ! Number . isFinite ( startValue ) || ! Number . isFinite ( endValue ) ) return Number . NaN ;
953+ const start = Math . floor ( startValue ) ;
954+ const end = Math . floor ( endValue ) ;
955+ const direction = start <= end ? 1 : - 1 ;
956+ const holidaySet = new Set ( holidays . map ( Math . floor ) ) ;
957+ let count = 0 ;
958+ for ( let serial = start ; direction > 0 ? serial <= end : serial >= end ; serial += direction ) {
959+ if ( isExcelWorkingDay ( serial , holidaySet ) ) count += direction ;
960+ }
961+ return count ;
962+ }
963+
964+ function workdayFormulaNumber ( startValue : number , daysValue : number , holidays : number [ ] ) : number {
965+ if ( ! Number . isFinite ( startValue ) || ! Number . isFinite ( daysValue ) ) return Number . NaN ;
966+ const direction = daysValue < 0 ? - 1 : 1 ;
967+ let remaining = Math . abs ( Math . trunc ( daysValue ) ) ;
968+ let serial = Math . floor ( startValue ) ;
969+ const holidaySet = new Set ( holidays . map ( Math . floor ) ) ;
970+ while ( remaining > 0 ) {
971+ serial += direction ;
972+ if ( isExcelWorkingDay ( serial , holidaySet ) ) remaining -= 1 ;
973+ }
974+ return serial ;
975+ }
976+
977+ function datedifFormulaNumber ( startValue : number , endValue : number , unit : string ) : number {
978+ const start = excelSerialUtcDate ( startValue ) ;
979+ const end = excelSerialUtcDate ( endValue ) ;
980+ if ( ! start || ! end || endValue < startValue ) return Number . NaN ;
981+ const normalizedUnit = unit . trim ( ) . toUpperCase ( ) ;
982+ if ( normalizedUnit === "D" ) return Math . floor ( endValue ) - Math . floor ( startValue ) ;
983+
984+ const years = end . getUTCFullYear ( ) - start . getUTCFullYear ( ) ;
985+ const monthDelta = years * 12 + end . getUTCMonth ( ) - start . getUTCMonth ( ) - ( end . getUTCDate ( ) < start . getUTCDate ( ) ? 1 : 0 ) ;
986+ if ( normalizedUnit === "M" ) return Math . max ( 0 , monthDelta ) ;
987+ if ( normalizedUnit === "Y" ) return Math . max ( 0 , Math . floor ( monthDelta / 12 ) ) ;
988+ return Number . NaN ;
989+ }
990+
991+ function isExcelWorkingDay ( serial : number , holidays : ReadonlySet < number > ) : boolean {
992+ const weekday = excelSerialDatePart ( serial , "WEEKDAY" , 2 ) ;
993+ return weekday >= 1 && weekday <= 5 && ! holidays . has ( serial ) ;
994+ }
995+
996+ function daysInUtcMonth ( year : number , month : number ) : number {
997+ return new Date ( Date . UTC ( year , month , 0 ) ) . getUTCDate ( ) ;
998+ }
999+
1000+ function modulo ( value : number , divisor : number ) : number {
1001+ return ( ( value % divisor ) + divisor ) % divisor ;
1002+ }
1003+
8941004function parseFunctionCall ( value : string ) : { args : string [ ] ; name : string } | null {
8951005 const nameMatch = value . match ( / ^ ( [ A - Z ] [ A - Z 0 - 9 . ] * ) \( / i) ;
8961006 if ( ! nameMatch || ! value . endsWith ( ")" ) ) return null ;
0 commit comments