Skip to content

Commit 39fce96

Browse files
committed
ref qax-os#65: new formula function WORKDAY.INTL
1 parent f038b14 commit 39fce96

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

calc.go

+192
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ type formulaFuncs struct {
724724
// WEEKNUM
725725
// WEIBULL
726726
// WEIBULL.DIST
727+
// WORKDAY.INTL
727728
// XIRR
728729
// XLOOKUP
729730
// XNPV
@@ -12552,6 +12553,197 @@ func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg {
1255212553
return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Month()))
1255312554
}
1255412555

12556+
// genWeekendMask generate weekend mask of a series of seven 0's and 1's which
12557+
// represent the seven weekdays, starting from Monday.
12558+
func genWeekendMask(weekend int) []byte {
12559+
mask := make([]byte, 7)
12560+
if masks, ok := map[int][]int{
12561+
1: {5, 6}, 2: {6, 0}, 3: {0, 1}, 4: {1, 2}, 5: {2, 3}, 6: {3, 4}, 7: {4, 5},
12562+
11: {6}, 12: {0}, 13: {1}, 14: {2}, 15: {3}, 16: {4}, 17: {5},
12563+
}[weekend]; ok {
12564+
for _, idx := range masks {
12565+
mask[idx] = 1
12566+
}
12567+
}
12568+
return mask
12569+
}
12570+
12571+
// isWorkday check if the date is workday.
12572+
func isWorkday(weekendMask []byte, date float64) bool {
12573+
dateTime := timeFromExcelTime(date, false)
12574+
weekday := dateTime.Weekday()
12575+
if weekday == time.Sunday {
12576+
weekday = 7
12577+
}
12578+
return weekendMask[weekday-1] == 0
12579+
}
12580+
12581+
// prepareWorkday returns weekend mask and workdays pre week by given days
12582+
// counted as weekend.
12583+
func prepareWorkday(weekend formulaArg) ([]byte, int) {
12584+
weekendArg := weekend.ToNumber()
12585+
if weekendArg.Type != ArgNumber {
12586+
return nil, 0
12587+
}
12588+
var weekendMask []byte
12589+
var workdaysPerWeek int
12590+
if len(weekend.Value()) == 7 {
12591+
// possible string values for the weekend argument
12592+
for _, mask := range weekend.Value() {
12593+
if mask != '0' && mask != '1' {
12594+
return nil, 0
12595+
}
12596+
weekendMask = append(weekendMask, byte(mask)-48)
12597+
}
12598+
} else {
12599+
weekendMask = genWeekendMask(int(weekendArg.Number))
12600+
}
12601+
for _, mask := range weekendMask {
12602+
if mask == 0 {
12603+
workdaysPerWeek++
12604+
}
12605+
}
12606+
return weekendMask, workdaysPerWeek
12607+
}
12608+
12609+
// toExcelDateArg function converts a text representation of a time, into an
12610+
// Excel date time number formula argument.
12611+
func toExcelDateArg(arg formulaArg) formulaArg {
12612+
num := arg.ToNumber()
12613+
if num.Type != ArgNumber {
12614+
dateString := strings.ToLower(arg.Value())
12615+
if !isDateOnlyFmt(dateString) {
12616+
if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
12617+
return err
12618+
}
12619+
}
12620+
y, m, d, _, err := strToDate(dateString)
12621+
if err.Type == ArgError {
12622+
return err
12623+
}
12624+
num.Number, _ = timeToExcelTime(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC), false)
12625+
return newNumberFormulaArg(num.Number)
12626+
}
12627+
if arg.Number < 0 {
12628+
return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
12629+
}
12630+
return num
12631+
}
12632+
12633+
// prepareHolidays function converts array type formula arguments to into an
12634+
// Excel date time number formula arguments list.
12635+
func prepareHolidays(args formulaArg) []int {
12636+
var holidays []int
12637+
for _, arg := range args.ToList() {
12638+
num := toExcelDateArg(arg)
12639+
if num.Type != ArgNumber {
12640+
continue
12641+
}
12642+
holidays = append(holidays, int(math.Ceil(num.Number)))
12643+
}
12644+
return holidays
12645+
}
12646+
12647+
// workdayIntl is an implementation of the formula function WORKDAY.INTL.
12648+
func workdayIntl(endDate, sign int, holidays []int, weekendMask []byte, startDate float64) int {
12649+
for i := 0; i < len(holidays); i++ {
12650+
holiday := holidays[i]
12651+
if sign > 0 {
12652+
if holiday > endDate {
12653+
break
12654+
}
12655+
} else {
12656+
if holiday < endDate {
12657+
break
12658+
}
12659+
}
12660+
if sign > 0 {
12661+
if holiday > int(math.Ceil(startDate)) {
12662+
if isWorkday(weekendMask, float64(holiday)) {
12663+
endDate += sign
12664+
for !isWorkday(weekendMask, float64(endDate)) {
12665+
endDate += sign
12666+
}
12667+
}
12668+
}
12669+
} else {
12670+
if holiday < int(math.Ceil(startDate)) {
12671+
if isWorkday(weekendMask, float64(holiday)) {
12672+
endDate += sign
12673+
for !isWorkday(weekendMask, float64(endDate)) {
12674+
endDate += sign
12675+
}
12676+
}
12677+
}
12678+
}
12679+
}
12680+
return endDate
12681+
}
12682+
12683+
// WORKDAYdotINTL function returns a date that is a supplied number of working
12684+
// days (excluding weekends and holidays) ahead of a given start date. The
12685+
// function allows the user to specify which days of the week are counted as
12686+
// weekends. The syntax of the function is:
12687+
//
12688+
// WORKDAY.INTL(start_date,days,[weekend],[holidays])
12689+
//
12690+
func (fn *formulaFuncs) WORKDAYdotINTL(argsList *list.List) formulaArg {
12691+
if argsList.Len() < 2 {
12692+
return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at least 2 arguments")
12693+
}
12694+
if argsList.Len() > 4 {
12695+
return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at most 4 arguments")
12696+
}
12697+
startDate := toExcelDateArg(argsList.Front().Value.(formulaArg))
12698+
if startDate.Type != ArgNumber {
12699+
return startDate
12700+
}
12701+
days := argsList.Front().Next().Value.(formulaArg).ToNumber()
12702+
if days.Type != ArgNumber {
12703+
return days
12704+
}
12705+
weekend := newNumberFormulaArg(1)
12706+
if argsList.Len() > 2 {
12707+
weekend = argsList.Front().Next().Next().Value.(formulaArg)
12708+
}
12709+
var holidays []int
12710+
if argsList.Len() == 4 {
12711+
holidays = prepareHolidays(argsList.Back().Value.(formulaArg))
12712+
sort.Ints(holidays)
12713+
}
12714+
if days.Number == 0 {
12715+
return newNumberFormulaArg(math.Ceil(startDate.Number))
12716+
}
12717+
weekendMask, workdaysPerWeek := prepareWorkday(weekend)
12718+
if workdaysPerWeek == 0 {
12719+
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
12720+
}
12721+
sign := 1
12722+
if days.Number < 0 {
12723+
sign = -1
12724+
}
12725+
offset := int(days.Number) / workdaysPerWeek
12726+
daysMod := int(days.Number) % workdaysPerWeek
12727+
endDate := int(math.Ceil(startDate.Number)) + offset*7
12728+
if daysMod == 0 {
12729+
for !isWorkday(weekendMask, float64(endDate)) {
12730+
endDate -= sign
12731+
}
12732+
} else {
12733+
for daysMod != 0 {
12734+
endDate += sign
12735+
if isWorkday(weekendMask, float64(endDate)) {
12736+
if daysMod < 0 {
12737+
daysMod++
12738+
continue
12739+
}
12740+
daysMod--
12741+
}
12742+
}
12743+
}
12744+
return newNumberFormulaArg(float64(workdayIntl(endDate, sign, holidays, weekendMask, startDate.Number)))
12745+
}
12746+
1255512747
// YEAR function returns an integer representing the year of a supplied date.
1255612748
// The syntax of the function is:
1255712749
//

calc_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -5379,6 +5379,72 @@ func TestCalcTTEST(t *testing.T) {
53795379
}
53805380
}
53815381

5382+
func TestCalcWORKDAYdotINTL(t *testing.T) {
5383+
cellData := [][]interface{}{
5384+
{"05/01/2019", 43586},
5385+
{"09/13/2019", 43721},
5386+
{"10/01/2019", 43739},
5387+
{"12/25/2019", 43824},
5388+
{"01/01/2020", 43831},
5389+
{"01/01/2020", 43831},
5390+
{"01/24/2020", 43854},
5391+
{"04/04/2020", 43925},
5392+
{"05/01/2020", 43952},
5393+
{"06/25/2020", 44007},
5394+
}
5395+
f := prepareCalcData(cellData)
5396+
formulaList := map[string]string{
5397+
"=WORKDAY.INTL(\"12/01/2015\",0)": "42339",
5398+
"=WORKDAY.INTL(\"12/01/2015\",25)": "42374",
5399+
"=WORKDAY.INTL(\"12/01/2015\",-25)": "42304",
5400+
"=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374",
5401+
"=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374",
5402+
"=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372",
5403+
"=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373",
5404+
"=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374",
5405+
"=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374",
5406+
"=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374",
5407+
"=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368",
5408+
"=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368",
5409+
"=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368",
5410+
"=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369",
5411+
"=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368",
5412+
"=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368",
5413+
"=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368",
5414+
"=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374",
5415+
"=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659",
5416+
"=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002",
5417+
"=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659",
5418+
"=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658",
5419+
"=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657",
5420+
"=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008",
5421+
"=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008",
5422+
}
5423+
for formula, expected := range formulaList {
5424+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5425+
result, err := f.CalcCellValue("Sheet1", "C1")
5426+
assert.NoError(t, err, formula)
5427+
assert.Equal(t, expected, result, formula)
5428+
}
5429+
calcError := map[string]string{
5430+
"=WORKDAY.INTL()": "WORKDAY.INTL requires at least 2 arguments",
5431+
"=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": "WORKDAY.INTL requires at most 4 arguments",
5432+
"=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": "strconv.ParseFloat: parsing \"\": invalid syntax",
5433+
"=WORKDAY.INTL(\"\",123,4,B1:B12)": "#VALUE!",
5434+
"=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": "#VALUE!",
5435+
"=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": "#VALUE!",
5436+
"=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": "#VALUE!",
5437+
"=WORKDAY.INTL(\"January 25, 100\",123)": "#VALUE!",
5438+
"=WORKDAY.INTL(-1,123)": "#NUM!",
5439+
}
5440+
for formula, expected := range calcError {
5441+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
5442+
result, err := f.CalcCellValue("Sheet1", "C1")
5443+
assert.EqualError(t, err, expected, formula)
5444+
assert.Equal(t, "", result, formula)
5445+
}
5446+
}
5447+
53825448
func TestCalcZTEST(t *testing.T) {
53835449
f := NewFile()
53845450
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{4, 5, 2, 5, 8, 9, 3, 2, 3, 8, 9, 5}))

0 commit comments

Comments
 (0)