Skip to content

Commit e101c71

Browse files
committed
ref qax-os#65, new formula functions: NETWORKDAYS, NETWORKDAYS.INTL, and WORKDAY
1 parent ea5b156 commit e101c71

File tree

4 files changed

+203
-50
lines changed

4 files changed

+203
-50
lines changed

calc.go

+118-8
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,8 @@ type formulaFuncs struct {
582582
// NA
583583
// NEGBINOM.DIST
584584
// NEGBINOMDIST
585+
// NETWORKDAYS
586+
// NETWORKDAYS.INTL
585587
// NOMINAL
586588
// NORM.DIST
587589
// NORMDIST
@@ -724,6 +726,7 @@ type formulaFuncs struct {
724726
// WEEKNUM
725727
// WEIBULL
726728
// WEIBULL.DIST
729+
// WORKDAY
727730
// WORKDAY.INTL
728731
// XIRR
729732
// XLOOKUP
@@ -899,12 +902,11 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg,
899902
if result.Type == ArgUnknown {
900903
return newEmptyFormulaArg(), errors.New(formulaErrorVALUE)
901904
}
902-
// when thisToken is Range and nextToken is Argument and opfdStack not Empty, should push value to opfdStack and continue.
903-
if nextToken.TType == efp.TokenTypeArgument {
904-
if !opfdStack.Empty() {
905-
opfdStack.Push(result)
906-
continue
907-
}
905+
// when current token is range, next token is argument and opfdStack not empty,
906+
// should push value to opfdStack and continue
907+
if nextToken.TType == efp.TokenTypeArgument && !opfdStack.Empty() {
908+
opfdStack.Push(result)
909+
continue
908910
}
909911
argsStack.Peek().(*list.List).PushBack(result)
910912
continue
@@ -12563,16 +12565,17 @@ func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg {
1256312565
// genWeekendMask generate weekend mask of a series of seven 0's and 1's which
1256412566
// represent the seven weekdays, starting from Monday.
1256512567
func genWeekendMask(weekend int) []byte {
12566-
mask := make([]byte, 7)
1256712568
if masks, ok := map[int][]int{
1256812569
1: {5, 6}, 2: {6, 0}, 3: {0, 1}, 4: {1, 2}, 5: {2, 3}, 6: {3, 4}, 7: {4, 5},
1256912570
11: {6}, 12: {0}, 13: {1}, 14: {2}, 15: {3}, 16: {4}, 17: {5},
1257012571
}[weekend]; ok {
12572+
mask := make([]byte, 7)
1257112573
for _, idx := range masks {
1257212574
mask[idx] = 1
1257312575
}
12576+
return mask
1257412577
}
12575-
return mask
12578+
return nil
1257612579
}
1257712580

1257812581
// isWorkday check if the date is workday.
@@ -12687,6 +12690,113 @@ func workdayIntl(endDate, sign int, holidays []int, weekendMask []byte, startDat
1268712690
return endDate
1268812691
}
1268912692

12693+
// NETWORKDAYS function calculates the number of work days between two supplied
12694+
// dates (including the start and end date). The calculation includes all
12695+
// weekdays (Mon - Fri), excluding a supplied list of holidays. The syntax of
12696+
// the function is:
12697+
//
12698+
// NETWORKDAYS(start_date,end_date,[holidays])
12699+
//
12700+
func (fn *formulaFuncs) NETWORKDAYS(argsList *list.List) formulaArg {
12701+
if argsList.Len() < 2 {
12702+
return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at least 2 arguments")
12703+
}
12704+
if argsList.Len() > 3 {
12705+
return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at most 3 arguments")
12706+
}
12707+
args := list.New()
12708+
args.PushBack(argsList.Front().Value.(formulaArg))
12709+
args.PushBack(argsList.Front().Next().Value.(formulaArg))
12710+
args.PushBack(newNumberFormulaArg(1))
12711+
if argsList.Len() == 3 {
12712+
args.PushBack(argsList.Back().Value.(formulaArg))
12713+
}
12714+
return fn.NETWORKDAYSdotINTL(args)
12715+
}
12716+
12717+
// NETWORKDAYSdotINTL function calculates the number of whole work days between
12718+
// two supplied dates, excluding weekends and holidays. The function allows
12719+
// the user to specify which days are counted as weekends and holidays. The
12720+
// syntax of the function is:
12721+
//
12722+
// NETWORKDAYS.INTL(start_date,end_date,[weekend],[holidays])
12723+
//
12724+
func (fn *formulaFuncs) NETWORKDAYSdotINTL(argsList *list.List) formulaArg {
12725+
if argsList.Len() < 2 {
12726+
return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at least 2 arguments")
12727+
}
12728+
if argsList.Len() > 4 {
12729+
return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at most 4 arguments")
12730+
}
12731+
startDate := toExcelDateArg(argsList.Front().Value.(formulaArg))
12732+
if startDate.Type != ArgNumber {
12733+
return startDate
12734+
}
12735+
endDate := toExcelDateArg(argsList.Front().Next().Value.(formulaArg))
12736+
if endDate.Type != ArgNumber {
12737+
return endDate
12738+
}
12739+
weekend := newNumberFormulaArg(1)
12740+
if argsList.Len() > 2 {
12741+
weekend = argsList.Front().Next().Next().Value.(formulaArg)
12742+
}
12743+
var holidays []int
12744+
if argsList.Len() == 4 {
12745+
holidays = prepareHolidays(argsList.Back().Value.(formulaArg))
12746+
sort.Ints(holidays)
12747+
}
12748+
weekendMask, workdaysPerWeek := prepareWorkday(weekend)
12749+
if workdaysPerWeek == 0 {
12750+
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
12751+
}
12752+
sign := 1
12753+
if startDate.Number > endDate.Number {
12754+
sign = -1
12755+
temp := startDate.Number
12756+
startDate.Number = endDate.Number
12757+
endDate.Number = temp
12758+
}
12759+
offset := endDate.Number - startDate.Number
12760+
count := int(math.Floor(offset/7) * float64(workdaysPerWeek))
12761+
daysMod := int(offset) % 7
12762+
for daysMod >= 0 {
12763+
if isWorkday(weekendMask, endDate.Number-float64(daysMod)) {
12764+
count++
12765+
}
12766+
daysMod--
12767+
}
12768+
for i := 0; i < len(holidays); i++ {
12769+
holiday := float64(holidays[i])
12770+
if isWorkday(weekendMask, holiday) && holiday >= startDate.Number && holiday <= endDate.Number {
12771+
count--
12772+
}
12773+
}
12774+
return newNumberFormulaArg(float64(sign * count))
12775+
}
12776+
12777+
// WORKDAY function returns a date that is a supplied number of working days
12778+
// (excluding weekends and holidays) ahead of a given start date. The syntax
12779+
// of the function is:
12780+
//
12781+
// WORKDAY(start_date,days,[holidays])
12782+
//
12783+
func (fn *formulaFuncs) WORKDAY(argsList *list.List) formulaArg {
12784+
if argsList.Len() < 2 {
12785+
return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at least 2 arguments")
12786+
}
12787+
if argsList.Len() > 3 {
12788+
return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at most 3 arguments")
12789+
}
12790+
args := list.New()
12791+
args.PushBack(argsList.Front().Value.(formulaArg))
12792+
args.PushBack(argsList.Front().Next().Value.(formulaArg))
12793+
args.PushBack(newNumberFormulaArg(1))
12794+
if argsList.Len() == 3 {
12795+
args.PushBack(argsList.Back().Value.(formulaArg))
12796+
}
12797+
return fn.WORKDAYdotINTL(args)
12798+
}
12799+
1269012800
// WORKDAYdotINTL function returns a date that is a supplied number of working
1269112801
// days (excluding weekends and holidays) ahead of a given start date. The
1269212802
// function allows the user to specify which days of the week are counted as

calc_test.go

+76-35
Original file line numberDiff line numberDiff line change
@@ -5380,7 +5380,7 @@ func TestCalcTTEST(t *testing.T) {
53805380
}
53815381
}
53825382

5383-
func TestCalcWORKDAYdotINTL(t *testing.T) {
5383+
func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) {
53845384
cellData := [][]interface{}{
53855385
{"05/01/2019", 43586},
53865386
{"09/13/2019", 43721},
@@ -5395,31 +5395,53 @@ func TestCalcWORKDAYdotINTL(t *testing.T) {
53955395
}
53965396
f := prepareCalcData(cellData)
53975397
formulaList := map[string]string{
5398-
"=WORKDAY.INTL(\"12/01/2015\",0)": "42339",
5399-
"=WORKDAY.INTL(\"12/01/2015\",25)": "42374",
5400-
"=WORKDAY.INTL(\"12/01/2015\",-25)": "42304",
5401-
"=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374",
5402-
"=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374",
5403-
"=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372",
5404-
"=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373",
5405-
"=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374",
5406-
"=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374",
5407-
"=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374",
5408-
"=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368",
5409-
"=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368",
5410-
"=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368",
5411-
"=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369",
5412-
"=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368",
5413-
"=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368",
5414-
"=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368",
5415-
"=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374",
5416-
"=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659",
5417-
"=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002",
5418-
"=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659",
5419-
"=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658",
5420-
"=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657",
5421-
"=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008",
5422-
"=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008",
5398+
"=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\")": "183",
5399+
"=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2)": "183",
5400+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\")": "183",
5401+
"=NETWORKDAYS.INTL(\"09/12/2020\",\"01/01/2020\")": "-183",
5402+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1)": "183",
5403+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",2)": "184",
5404+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",3)": "184",
5405+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4)": "183",
5406+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",5)": "182",
5407+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",6)": "182",
5408+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",7)": "182",
5409+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",11)": "220",
5410+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",12)": "220",
5411+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",13)": "220",
5412+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",14)": "219",
5413+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",15)": "219",
5414+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",16)": "219",
5415+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",17)": "219",
5416+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,A1:A12)": "178",
5417+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,B1:B12)": "178",
5418+
"=WORKDAY(\"12/01/2015\",25)": "42374",
5419+
"=WORKDAY(\"01/01/2020\",123,B1:B12)": "44006",
5420+
"=WORKDAY.INTL(\"12/01/2015\",0)": "42339",
5421+
"=WORKDAY.INTL(\"12/01/2015\",25)": "42374",
5422+
"=WORKDAY.INTL(\"12/01/2015\",-25)": "42304",
5423+
"=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374",
5424+
"=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374",
5425+
"=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372",
5426+
"=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373",
5427+
"=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374",
5428+
"=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374",
5429+
"=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374",
5430+
"=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368",
5431+
"=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368",
5432+
"=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368",
5433+
"=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369",
5434+
"=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368",
5435+
"=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368",
5436+
"=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368",
5437+
"=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374",
5438+
"=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659",
5439+
"=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002",
5440+
"=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659",
5441+
"=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658",
5442+
"=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657",
5443+
"=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008",
5444+
"=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008",
54235445
}
54245446
for formula, expected := range formulaList {
54255447
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
@@ -5428,15 +5450,34 @@ func TestCalcWORKDAYdotINTL(t *testing.T) {
54285450
assert.Equal(t, expected, result, formula)
54295451
}
54305452
calcError := map[string]string{
5431-
"=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments",
5432-
"=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments",
5433-
"=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax",
5434-
"=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!",
5435-
"=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!",
5436-
"=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!",
5437-
"=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!",
5438-
"=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!",
5439-
"=WORKDAY.INTL(-1,123)": "#NUM!",
5453+
"=NETWORKDAYS()": "NETWORKDAYS requires at least 2 arguments",
5454+
"=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2,\"\")": "NETWORKDAYS requires at most 3 arguments",
5455+
"=NETWORKDAYS(\"\",\"09/12/2020\",2)": "#VALUE!",
5456+
"=NETWORKDAYS(\"01/01/2020\",\"\",2)": "#VALUE!",
5457+
"=NETWORKDAYS.INTL()": "NETWORKDAYS.INTL requires at least 2 arguments",
5458+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4,A1:A12,\"\")": "NETWORKDAYS.INTL requires at most 4 arguments",
5459+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"January 25, 100\",4)": "#VALUE!",
5460+
"=NETWORKDAYS.INTL(\"\",123,4,B1:B12)": "#VALUE!",
5461+
"=NETWORKDAYS.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!",
5462+
"=NETWORKDAYS.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!",
5463+
"=NETWORKDAYS.INTL(\"January 25, 100\",123)": "#VALUE!",
5464+
"=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",8)": "#VALUE!",
5465+
"=NETWORKDAYS.INTL(-1,123)": "#NUM!",
5466+
"=WORKDAY()": "WORKDAY requires at least 2 arguments",
5467+
"=WORKDAY(\"01/01/2020\",123,A1:A12,\"\")": "WORKDAY requires at most 3 arguments",
5468+
"=WORKDAY(\"01/01/2020\",\"\",B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax",
5469+
"=WORKDAY(\"\",123,B1:B12)": "#VALUE!",
5470+
"=WORKDAY(\"January 25, 100\",123)": "#VALUE!",
5471+
"=WORKDAY(-1,123)": "#NUM!",
5472+
"=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments",
5473+
"=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments",
5474+
"=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax",
5475+
"=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!",
5476+
"=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!",
5477+
"=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!",
5478+
"=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!",
5479+
"=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!",
5480+
"=WORKDAY.INTL(-1,123)": "#NUM!",
54405481
}
54415482
for formula, expected := range calcError {
54425483
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))

datavalidation_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func TestDeleteDataValidation(t *testing.T) {
172172
// Test delete data validation on no exists worksheet.
173173
assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist")
174174

175-
// Test delete all data validations in the worksheet
175+
// Test delete all data validations in the worksheet.
176176
assert.NoError(t, f.DeleteDataValidation("Sheet1"))
177177
assert.Nil(t, ws.(*xlsxWorksheet).DataValidations)
178178
}

rows.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ import (
2828

2929
// GetRows return all the rows in a sheet by given worksheet name
3030
// (case sensitive), returned as a two-dimensional array, where the value of
31-
// the cell is converted to the string type. If the cell format can be
32-
// applied to the value of the cell, the applied value will be used,
33-
// otherwise the original value will be used. GetRows fetched the rows with
34-
// value or formula cells, the tail continuously empty cell will be skipped.
35-
// For example:
31+
// the cell is converted to the string type. If the cell format can be applied
32+
// to the value of the cell, the applied value will be used, otherwise the
33+
// original value will be used. GetRows fetched the rows with value or formula
34+
// cells, the continually blank cells in the tail of each row will be skipped,
35+
// so the length of each row may be inconsistent. For example:
3636
//
3737
// rows, err := f.GetRows("Sheet1")
3838
// if err != nil {
@@ -122,7 +122,9 @@ func (rows *Rows) Close() error {
122122
return nil
123123
}
124124

125-
// Columns return the current row's column values.
125+
// Columns return the current row's column values. This fetches the worksheet
126+
// data as a stream, returns each cell in a row as is, and will not skip empty
127+
// rows in the tail of the worksheet.
126128
func (rows *Rows) Columns(opts ...Options) ([]string, error) {
127129
if rows.curRow > rows.seekRow {
128130
return nil, nil

0 commit comments

Comments
 (0)