Skip to content

Commit e570a23

Browse files
committed
ref qax-os#65, formula functions TEXTAFTER and TEXTBEFORE (array formula not support yet)
1 parent 3aa039f commit e570a23

File tree

3 files changed

+231
-8
lines changed

3 files changed

+231
-8
lines changed

calc.go

+165-7
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ func (fa formulaArg) ToBool() formulaArg {
314314
return newErrorFormulaArg(formulaErrorVALUE, err.Error())
315315
}
316316
case ArgNumber:
317-
if fa.Boolean && fa.Number == 1 {
317+
if fa.Number == 1 {
318318
b = true
319319
}
320320
}
@@ -758,6 +758,8 @@ type formulaFuncs struct {
758758
// TBILLYIELD
759759
// TDIST
760760
// TEXT
761+
// TEXTAFTER
762+
// TEXTBEFORE
761763
// TEXTJOIN
762764
// TIME
763765
// TIMEVALUE
@@ -13748,7 +13750,7 @@ func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg {
1374813750
if argsList.Len() != 1 {
1374913751
return newErrorFormulaArg(formulaErrorVALUE, "LEN requires 1 string argument")
1375013752
}
13751-
return newStringFormulaArg(strconv.Itoa(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).String)))
13753+
return newNumberFormulaArg(float64(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).String)))
1375213754
}
1375313755

1375413756
// LENB returns the number of bytes used to represent the characters in a text
@@ -13770,7 +13772,7 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg {
1377013772
bytes += 2
1377113773
}
1377213774
}
13773-
return newStringFormulaArg(strconv.Itoa(bytes))
13775+
return newNumberFormulaArg(float64(bytes))
1377413776
}
1377513777

1377613778
// LOWER converts all characters in a supplied text string to lower case. The
@@ -14058,6 +14060,163 @@ func (fn *formulaFuncs) TEXT(argsList *list.List) formulaArg {
1405814060
return newStringFormulaArg(format(value.Value(), fmtText.Value(), false, cellType, nil))
1405914061
}
1406014062

14063+
// prepareTextAfterBefore checking and prepare arguments for the formula
14064+
// functions TEXTAFTER and TEXTBEFORE.
14065+
func (fn *formulaFuncs) prepareTextAfterBefore(name string, argsList *list.List) formulaArg {
14066+
argsLen := argsList.Len()
14067+
if argsLen < 2 {
14068+
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
14069+
}
14070+
if argsLen > 6 {
14071+
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s accepts at most 6 arguments", name))
14072+
}
14073+
text, delimiter := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg)
14074+
instanceNum, matchMode, matchEnd, ifNotFound := newNumberFormulaArg(1), newBoolFormulaArg(false), newBoolFormulaArg(false), newEmptyFormulaArg()
14075+
if argsLen > 2 {
14076+
instanceNum = argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
14077+
if instanceNum.Type != ArgNumber {
14078+
return instanceNum
14079+
}
14080+
}
14081+
if argsLen > 3 {
14082+
matchMode = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool()
14083+
if matchMode.Type != ArgNumber {
14084+
return matchMode
14085+
}
14086+
if matchMode.Number == 1 {
14087+
text, delimiter = newStringFormulaArg(strings.ToLower(text.Value())), newStringFormulaArg(strings.ToLower(delimiter.Value()))
14088+
}
14089+
}
14090+
if argsLen > 4 {
14091+
matchEnd = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToBool()
14092+
if matchEnd.Type != ArgNumber {
14093+
return matchEnd
14094+
}
14095+
}
14096+
if argsLen > 5 {
14097+
ifNotFound = argsList.Back().Value.(formulaArg)
14098+
}
14099+
if text.Value() == "" {
14100+
return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
14101+
}
14102+
lenArgsList := list.New().Init()
14103+
lenArgsList.PushBack(text)
14104+
textLen := fn.LEN(lenArgsList)
14105+
if instanceNum.Number == 0 || instanceNum.Number > textLen.Number {
14106+
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
14107+
}
14108+
reverseSearch, startPos := instanceNum.Number < 0, 0.0
14109+
if reverseSearch {
14110+
startPos = textLen.Number
14111+
}
14112+
return newListFormulaArg([]formulaArg{
14113+
text, delimiter, instanceNum, matchMode, matchEnd, ifNotFound,
14114+
textLen, newBoolFormulaArg(reverseSearch), newNumberFormulaArg(startPos),
14115+
})
14116+
}
14117+
14118+
// textAfterBeforeSearch is an implementation of the formula functions TEXTAFTER
14119+
// and TEXTBEFORE.
14120+
func textAfterBeforeSearch(text string, delimiter []string, startPos int, reverseSearch bool) (int, string) {
14121+
idx := -1
14122+
var modifiedDelimiter string
14123+
for i := 0; i < len(delimiter); i++ {
14124+
nextDelimiter := delimiter[i]
14125+
nextIdx := strings.Index(text[startPos:], nextDelimiter)
14126+
if nextIdx != -1 {
14127+
nextIdx += startPos
14128+
}
14129+
if reverseSearch {
14130+
nextIdx = strings.LastIndex(text[:startPos], nextDelimiter)
14131+
}
14132+
if idx == -1 || (((nextIdx < idx && !reverseSearch) || (nextIdx > idx && reverseSearch)) && idx != -1) {
14133+
idx = nextIdx
14134+
modifiedDelimiter = nextDelimiter
14135+
}
14136+
}
14137+
return idx, modifiedDelimiter
14138+
}
14139+
14140+
// textAfterBeforeResult is an implementation of the formula functions TEXTAFTER
14141+
// and TEXTBEFORE.
14142+
func textAfterBeforeResult(name, modifiedDelimiter string, text []rune, foundIdx, repeatZero, textLen int, matchEndActive, matchEnd, reverseSearch bool) formulaArg {
14143+
if name == "TEXTAFTER" {
14144+
endPos := len(modifiedDelimiter)
14145+
if (repeatZero > 1 || matchEndActive) && matchEnd && reverseSearch {
14146+
endPos = 0
14147+
}
14148+
if foundIdx+endPos >= textLen {
14149+
return newEmptyFormulaArg()
14150+
}
14151+
return newStringFormulaArg(string(text[foundIdx+endPos : textLen]))
14152+
}
14153+
return newStringFormulaArg(string(text[:foundIdx]))
14154+
}
14155+
14156+
// textAfterBefore is an implementation of the formula functions TEXTAFTER and
14157+
// TEXTBEFORE.
14158+
func (fn *formulaFuncs) textAfterBefore(name string, argsList *list.List) formulaArg {
14159+
args := fn.prepareTextAfterBefore(name, argsList)
14160+
if args.Type != ArgList {
14161+
return args
14162+
}
14163+
var (
14164+
text = []rune(argsList.Front().Value.(formulaArg).Value())
14165+
modifiedText = args.List[0].Value()
14166+
delimiter = []string{args.List[1].Value()}
14167+
instanceNum = args.List[2].Number
14168+
matchEnd = args.List[4].Number == 1
14169+
ifNotFound = args.List[5]
14170+
textLen = args.List[6]
14171+
reverseSearch = args.List[7].Number == 1
14172+
foundIdx = -1
14173+
repeatZero, startPos int
14174+
matchEndActive bool
14175+
modifiedDelimiter string
14176+
)
14177+
if reverseSearch {
14178+
startPos = int(args.List[8].Number)
14179+
}
14180+
for i := 0; i < int(math.Abs(instanceNum)); i++ {
14181+
foundIdx, modifiedDelimiter = textAfterBeforeSearch(modifiedText, delimiter, startPos, reverseSearch)
14182+
if foundIdx == 0 {
14183+
repeatZero++
14184+
}
14185+
if foundIdx == -1 {
14186+
if matchEnd && i == int(math.Abs(instanceNum))-1 {
14187+
if foundIdx = int(textLen.Number); reverseSearch {
14188+
foundIdx = 0
14189+
}
14190+
matchEndActive = true
14191+
}
14192+
break
14193+
}
14194+
if startPos = foundIdx + len(modifiedDelimiter); reverseSearch {
14195+
startPos = foundIdx - len(modifiedDelimiter)
14196+
}
14197+
}
14198+
if foundIdx == -1 {
14199+
return ifNotFound
14200+
}
14201+
return textAfterBeforeResult(name, modifiedDelimiter, text, foundIdx, repeatZero, int(textLen.Number), matchEndActive, matchEnd, reverseSearch)
14202+
}
14203+
14204+
// TEXTAFTER function returns the text that occurs after a given substring or
14205+
// delimiter. The syntax of the function is:
14206+
//
14207+
// TEXTAFTER(text,delimiter,[instance_num],[match_mode],[match_end],[if_not_found])
14208+
func (fn *formulaFuncs) TEXTAFTER(argsList *list.List) formulaArg {
14209+
return fn.textAfterBefore("TEXTAFTER", argsList)
14210+
}
14211+
14212+
// TEXTBEFORE function returns text that occurs before a given character or
14213+
// string. The syntax of the function is:
14214+
//
14215+
// TEXTBEFORE(text,delimiter,[instance_num],[match_mode],[match_end],[if_not_found])
14216+
func (fn *formulaFuncs) TEXTBEFORE(argsList *list.List) formulaArg {
14217+
return fn.textAfterBefore("TEXTBEFORE", argsList)
14218+
}
14219+
1406114220
// TEXTJOIN function joins together a series of supplied text strings into one
1406214221
// combined text string. The user can specify a delimiter to add between the
1406314222
// individual text items, if required. The syntax of the function is:
@@ -14465,8 +14624,7 @@ func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool)
1446514624
return criteriaG
1446614625
}
1446714626
for i := range lhs.Matrix {
14468-
left := lhs.Matrix[i]
14469-
right := lhs.Matrix[i]
14627+
left, right := lhs.Matrix[i], rhs.Matrix[i]
1447014628
if len(left) < len(right) {
1447114629
return criteriaL
1447214630
}
@@ -15289,7 +15447,7 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg {
1528915447
}
1529015448
min, max := calcColsRowsMinMax(false, argsList)
1529115449
if max == TotalRows {
15292-
return newStringFormulaArg(strconv.Itoa(TotalRows))
15450+
return newNumberFormulaArg(TotalRows)
1529315451
}
1529415452
result := max - min + 1
1529515453
if max == min {
@@ -15298,7 +15456,7 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg {
1529815456
}
1529915457
return newNumberFormulaArg(float64(1))
1530015458
}
15301-
return newStringFormulaArg(strconv.Itoa(result))
15459+
return newNumberFormulaArg(float64(result))
1530215460
}
1530315461

1530415462
// Web Functions

calc_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,35 @@ func TestCalcCellValue(t *testing.T) {
18431843
"=TEXT(567.9,\"$#,##0.00\")": "$567.90",
18441844
"=TEXT(-5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "- $5.00",
18451845
"=TEXT(5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "+ $5.00",
1846+
// TEXTAFTER
1847+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\")": "'s, red hood",
1848+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"HOOD\",1,1)": "'s, red hood",
1849+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"basket\",1,0,0,\"x\")": "x",
1850+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"basket\",1,0,1,\"x\")": "",
1851+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",-1)": "",
1852+
"=TEXTAFTER(\"Jones,Bob\",\",\")": "Bob",
1853+
"=TEXTAFTER(\"12 ft x 20 ft\",\" x \")": "20 ft",
1854+
"=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",1)": "112-Red-Y",
1855+
"=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",2)": "Red-Y",
1856+
"=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-1)": "Y",
1857+
"=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-2)": "Red-Y",
1858+
"=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-3)": "112-Red-Y",
1859+
"=TEXTAFTER(\"ABX-123-Red-XYZ\",\"-\",-4,0,1)": "ABX-123-Red-XYZ",
1860+
"=TEXTAFTER(\"ABX-123-Red-XYZ\",\"A\")": "BX-123-Red-XYZ",
1861+
// TEXTBEFORE
1862+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\")": "Red riding ",
1863+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"HOOD\",1,1)": "Red riding ",
1864+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"basket\",1,0,0,\"x\")": "x",
1865+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"basket\",1,0,1,\"x\")": "Red riding hood's, red hood",
1866+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",-1)": "Red riding hood's, red ",
1867+
"=TEXTBEFORE(\"Jones,Bob\",\",\")": "Jones",
1868+
"=TEXTBEFORE(\"12 ft x 20 ft\",\" x \")": "12 ft",
1869+
"=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",1)": "ABX",
1870+
"=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",2)": "ABX-112",
1871+
"=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",-1)": "ABX-112-Red",
1872+
"=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",-2)": "ABX-112",
1873+
"=TEXTBEFORE(\"ABX-123-Red-XYZ\",\"-\",4,0,1)": "ABX-123-Red-XYZ",
1874+
"=TEXTBEFORE(\"ABX-112-Red-Y\",\"A\")": "",
18461875
// TEXTJOIN
18471876
"=TEXTJOIN(\"-\",TRUE,1,2,3,4)": "1-2-3-4",
18481877
"=TEXTJOIN(A4,TRUE,A1:B2)": "1040205",
@@ -3879,6 +3908,24 @@ func TestCalcCellValue(t *testing.T) {
38793908
"=TEXT()": {"#VALUE!", "TEXT requires 2 arguments"},
38803909
"=TEXT(NA(),\"\")": {"#N/A", "#N/A"},
38813910
"=TEXT(0,NA())": {"#N/A", "#N/A"},
3911+
// TEXTAFTER
3912+
"=TEXTAFTER()": {"#VALUE!", "TEXTAFTER requires at least 2 arguments"},
3913+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,0,0,\"\",0)": {"#VALUE!", "TEXTAFTER accepts at most 6 arguments"},
3914+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
3915+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
3916+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
3917+
"=TEXTAFTER(\"\",\"hood\")": {"#N/A", "#N/A"},
3918+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",0)": {"#VALUE!", "#VALUE!"},
3919+
"=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",28)": {"#VALUE!", "#VALUE!"},
3920+
// TEXTBEFORE
3921+
"=TEXTBEFORE()": {"#VALUE!", "TEXTBEFORE requires at least 2 arguments"},
3922+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,0,0,\"\",0)": {"#VALUE!", "TEXTBEFORE accepts at most 6 arguments"},
3923+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
3924+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
3925+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
3926+
"=TEXTBEFORE(\"\",\"hood\")": {"#N/A", "#N/A"},
3927+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",0)": {"#VALUE!", "#VALUE!"},
3928+
"=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",28)": {"#VALUE!", "#VALUE!"},
38823929
// TEXTJOIN
38833930
"=TEXTJOIN()": {"#VALUE!", "TEXTJOIN requires at least 3 arguments"},
38843931
"=TEXTJOIN(\"\",\"\",1)": {"#VALUE!", "#VALUE!"},
@@ -4739,9 +4786,27 @@ func TestCalcCompareFormulaArg(t *testing.T) {
47394786
rhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)})
47404787
assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaEq)
47414788

4789+
lhs = newListFormulaArg([]formulaArg{newNumberFormulaArg(1)})
4790+
rhs = newListFormulaArg([]formulaArg{newNumberFormulaArg(0)})
4791+
assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
4792+
47424793
assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr)
47434794
}
47444795

4796+
func TestCalcCompareFormulaArgMatrix(t *testing.T) {
4797+
lhs := newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg()}})
4798+
rhs := newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg(), newEmptyFormulaArg()}})
4799+
assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaL)
4800+
4801+
lhs = newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg(), newEmptyFormulaArg()}})
4802+
rhs = newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg()}})
4803+
assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
4804+
4805+
lhs = newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(1)}})
4806+
rhs = newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(0)}})
4807+
assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
4808+
}
4809+
47454810
func TestCalcTRANSPOSE(t *testing.T) {
47464811
cellData := [][]interface{}{
47474812
{"a", "d"},

pivotTable.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
343343
UseAutoFormatting: &opts.UseAutoFormatting,
344344
PageOverThenDown: &opts.PageOverThenDown,
345345
MergeItem: &opts.MergeItem,
346-
CreatedVersion: 3,
346+
CreatedVersion: pivotTableVersion,
347347
CompactData: &opts.CompactData,
348348
ShowError: &opts.ShowError,
349349
DataCaption: "Values",

0 commit comments

Comments
 (0)