فهرست منبع

#65 fn: LARGE, SMALL, SUBSTITUTE, refactor ARABIC

xuri 4 سال پیش
والد
کامیت
7ef1892f32
2فایلهای تغییر یافته به همراه178 افزوده شده و 30 حذف شده
  1. 142 26
      calc.go
  2. 36 4
      calc_test.go

+ 142 - 26
calc.go

@@ -167,15 +167,17 @@ func (fa formulaArg) ToBool() formulaArg {
 
 // ToList returns a formula argument with array data type.
 func (fa formulaArg) ToList() []formulaArg {
-	if fa.Type == ArgMatrix {
+	switch fa.Type {
+	case ArgMatrix:
 		list := []formulaArg{}
 		for _, row := range fa.Matrix {
 			list = append(list, row...)
 		}
 		return list
-	}
-	if fa.Type == ArgList {
+	case ArgList:
 		return fa.List
+	case ArgNumber, ArgString, ArgError, ArgUnknown:
+		return []formulaArg{fa}
 	}
 	return nil
 }
@@ -294,6 +296,7 @@ var tokenPriority = map[string]int{
 //    ISTEXT
 //    ISO.CEILING
 //    KURT
+//    LARGE
 //    LCM
 //    LEFT
 //    LEFTB
@@ -348,10 +351,12 @@ var tokenPriority = map[string]int{
 //    SIGN
 //    SIN
 //    SINH
+//    SMALL
 //    SQRT
 //    SQRTPI
 //    STDEV
 //    STDEVA
+//    SUBSTITUTE
 //    SUM
 //    SUMIF
 //    SUMSQ
@@ -454,10 +459,7 @@ func newEmptyFormulaArg() formulaArg {
 //    opf  - Operation formula
 //    opfd - Operand of the operation formula
 //    opft - Operator of the operation formula
-//
-// Evaluate arguments of the operation formula by list:
-//
-//    args - Arguments of the operation formula
+//    args - Arguments list of the operation formula
 //
 // TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union
 //
@@ -1705,28 +1707,48 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg {
 	if argsList.Len() != 1 {
 		return newErrorFormulaArg(formulaErrorVALUE, "ARABIC requires 1 numeric argument")
 	}
-	charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
-	val, last, prefix := 0.0, 0.0, 1.0
-	for _, char := range argsList.Front().Value.(formulaArg).String {
-		digit := 0.0
-		if char == '-' {
-			prefix = -1
+	text := argsList.Front().Value.(formulaArg).Value()
+	if len(text) > 255 {
+		return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+	}
+	text = strings.ToUpper(text)
+	number, actualStart, index, isNegative := 0, 0, len(text)-1, false
+	startIndex, subtractNumber, currentPartValue, currentCharValue, prevCharValue := 0, 0, 0, 0, -1
+	for index >= 0 && text[index] == ' ' {
+		index--
+	}
+	for actualStart <= index && text[actualStart] == ' ' {
+		actualStart++
+	}
+	if actualStart <= index && text[actualStart] == '-' {
+		isNegative = true
+		actualStart++
+	}
+	charMap := map[rune]int{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
+	for index >= actualStart {
+		startIndex = index
+		startChar := text[startIndex]
+		index--
+		for index >= actualStart && (text[index]|' ') == startChar {
+			index--
+		}
+		currentCharValue = charMap[rune(startChar)]
+		currentPartValue = (startIndex - index) * currentCharValue
+		if currentCharValue >= prevCharValue {
+			number += currentPartValue - subtractNumber
+			prevCharValue = currentCharValue
+			subtractNumber = 0
 			continue
 		}
-		digit = charMap[char]
-		val += digit
-		switch {
-		case last == digit && (last == 5 || last == 50 || last == 500):
-			return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
-		case 2*last == digit:
-			return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
-		}
-		if last < digit {
-			val -= 2 * last
-		}
-		last = digit
+		subtractNumber += currentPartValue
+	}
+	if subtractNumber != 0 {
+		number -= subtractNumber
+	}
+	if isNegative {
+		number = -number
 	}
-	return newNumberFormulaArg(prefix * val)
+	return newNumberFormulaArg(float64(number))
 }
 
 // ASIN function calculates the arcsine (i.e. the inverse sine) of a given
@@ -3933,6 +3955,45 @@ func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg {
 	return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
 }
 
+// kth is an implementation of the formula function LARGE and SMALL.
+func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg {
+	if argsList.Len() != 2 {
+		return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name))
+	}
+	array := argsList.Front().Value.(formulaArg).ToList()
+	kArg := argsList.Back().Value.(formulaArg).ToNumber()
+	if kArg.Type != ArgNumber {
+		return kArg
+	}
+	k := int(kArg.Number)
+	if k < 1 {
+		return newErrorFormulaArg(formulaErrorNUM, "k should be > 0")
+	}
+	data := []float64{}
+	for _, arg := range array {
+		if numArg := arg.ToNumber(); numArg.Type == ArgNumber {
+			data = append(data, numArg.Number)
+		}
+	}
+	if len(data) < k {
+		return newErrorFormulaArg(formulaErrorNUM, "k should be <= length of array")
+	}
+	sort.Float64s(data)
+	if name == "LARGE" {
+		return newNumberFormulaArg(data[len(data)-k])
+	}
+	return newNumberFormulaArg(data[k-1])
+}
+
+// LARGE function returns the k'th largest value from an array of numeric
+// values. The syntax of the function is:
+//
+//    LARGE(array,k)
+//
+func (fn *formulaFuncs) LARGE(argsList *list.List) formulaArg {
+	return fn.kth("LARGE", argsList)
+}
+
 // MAX function returns the largest value from a supplied set of numeric
 // values. The syntax of the function is:
 //
@@ -4168,6 +4229,15 @@ func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg {
 	return newNumberFormulaArg(math.Round(fact(number.Number) / fact(number.Number-chosen.Number)))
 }
 
+// SMALL function returns the k'th smallest value from an array of numeric
+// values. The syntax of the function is:
+//
+//    SMALL(array,k)
+//
+func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg {
+	return fn.kth("SMALL", argsList)
+}
+
 // Information Functions
 
 // ISBLANK function tests if a specified cell is blank (empty) and if so,
@@ -5031,6 +5101,52 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg {
 	return fn.leftRight("RIGHTB", argsList)
 }
 
+// SUBSTITUTE function replaces one or more instances of a given text string,
+// within an original text string. The syntax of the function is:
+//
+//    SUBSTITUTE(text,old_text,new_text,[instance_num])
+//
+func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg {
+	if argsList.Len() != 3 && argsList.Len() != 4 {
+		return newErrorFormulaArg(formulaErrorVALUE, "SUBSTITUTE requires 3 or 4 arguments")
+	}
+	text, oldText := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg)
+	newText, instanceNum := argsList.Front().Next().Next().Value.(formulaArg), 0
+	if argsList.Len() == 3 {
+		return newStringFormulaArg(strings.Replace(text.Value(), oldText.Value(), newText.Value(), -1))
+	}
+	instanceNumArg := argsList.Back().Value.(formulaArg).ToNumber()
+	if instanceNumArg.Type != ArgNumber {
+		return instanceNumArg
+	}
+	instanceNum = int(instanceNumArg.Number)
+	if instanceNum < 1 {
+		return newErrorFormulaArg(formulaErrorVALUE, "instance_num should be > 0")
+	}
+	str, oldTextLen, count, chars, pos := text.Value(), len(oldText.Value()), instanceNum, 0, -1
+	for {
+		count--
+		index := strings.Index(str, oldText.Value())
+		if index == -1 {
+			pos = -1
+			break
+		} else {
+			pos = index + chars
+			if count == 0 {
+				break
+			}
+			idx := oldTextLen + index
+			chars += idx
+			str = str[idx:]
+		}
+	}
+	if pos == -1 {
+		return newStringFormulaArg(text.Value())
+	}
+	pre, post := text.Value()[:pos], text.Value()[pos+oldTextLen:]
+	return newStringFormulaArg(pre + newText.Value() + post)
+}
+
 // TRIM removes extra spaces (i.e. all spaces except for single spaces between
 // words or characters) from a supplied text string. The syntax of the
 // function is:

+ 36 - 4
calc_test.go

@@ -161,10 +161,11 @@ func TestCalcCellValue(t *testing.T) {
 		"=_xlfn.ACOTH(2)":       "0.549306144334055",
 		"=_xlfn.ACOTH(ABS(-2))": "0.549306144334055",
 		// ARABIC
-		`=_xlfn.ARABIC("IV")`:   "4",
-		`=_xlfn.ARABIC("-IV")`:  "-4",
-		`=_xlfn.ARABIC("MCXX")`: "1120",
-		`=_xlfn.ARABIC("")`:     "0",
+		"=_xlfn.ARABIC(\"IV\")":       "4",
+		"=_xlfn.ARABIC(\"-IV\")":      "-4",
+		"=_xlfn.ARABIC(\"MCXX\")":     "1120",
+		"=_xlfn.ARABIC(\"\")":         "0",
+		"=_xlfn.ARABIC(\" ll  lc \")": "-50",
 		// ASIN
 		"=ASIN(-1)":      "-1.570796326794897",
 		"=ASIN(0)":       "0",
@@ -608,6 +609,11 @@ func TestCalcCellValue(t *testing.T) {
 		"=KURT(F1:F9)":           "-1.033503502551368",
 		"=KURT(F1,F2:F9)":        "-1.033503502551368",
 		"=KURT(INT(1),MUNIT(2))": "-3.333333333333336",
+		// LARGE
+		"=LARGE(A1:A5,1)": "3",
+		"=LARGE(A1:B5,2)": "4",
+		"=LARGE(A1,1)":    "1",
+		"=LARGE(A1:F2,1)": "36693",
 		// MAX
 		"=MAX(1)":          "1",
 		"=MAX(TRUE())":     "1",
@@ -646,6 +652,11 @@ func TestCalcCellValue(t *testing.T) {
 		"=PERMUT(6,6)":  "720",
 		"=PERMUT(7,6)":  "5040",
 		"=PERMUT(10,6)": "151200",
+		// SMALL
+		"=SMALL(A1:A5,1)": "0",
+		"=SMALL(A1:B5,2)": "1",
+		"=SMALL(A1,1)":    "1",
+		"=SMALL(A1:F2,1)": "1",
 		// Information Functions
 		// ISBLANK
 		"=ISBLANK(A1)": "FALSE",
@@ -814,6 +825,12 @@ func TestCalcCellValue(t *testing.T) {
 		"=RIGHTB(\"Original Text\",0)":  "",
 		"=RIGHTB(\"Original Text\",13)": "Original Text",
 		"=RIGHTB(\"Original Text\",20)": "Original Text",
+		// SUBSTITUTE
+		"=SUBSTITUTE(\"abab\",\"a\",\"X\")":                      "XbXb",
+		"=SUBSTITUTE(\"abab\",\"a\",\"X\",2)":                    "abXb",
+		"=SUBSTITUTE(\"abab\",\"x\",\"X\",2)":                    "abab",
+		"=SUBSTITUTE(\"John is 5 years old\",\"John\",\"Jack\")": "Jack is 5 years old",
+		"=SUBSTITUTE(\"John is 5 years old\",\"5\",\"6\")":       "John is 6 years old",
 		// TRIM
 		"=TRIM(\" trim text \")": "trim text",
 		"=TRIM(0)":               "0",
@@ -1046,6 +1063,7 @@ func TestCalcCellValue(t *testing.T) {
 		"=_xlfn.ACOTH(_xlfn.ACOTH(2))": "#NUM!",
 		// _xlfn.ARABIC
 		"=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument",
+		"=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": "#VALUE!",
 		// ASIN
 		"=ASIN()":    "ASIN requires 1 numeric argument",
 		`=ASIN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax",
@@ -1334,6 +1352,11 @@ func TestCalcCellValue(t *testing.T) {
 		// KURT
 		"=KURT()":          "KURT requires at least 1 argument",
 		"=KURT(F1,INT(1))": "#DIV/0!",
+		// LARGE
+		"=LARGE()":           "LARGE requires 2 arguments",
+		"=LARGE(A1:A5,0)":    "k should be > 0",
+		"=LARGE(A1:A5,6)":    "k should be <= length of array",
+		"=LARGE(A1:A5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
 		// MAX
 		"=MAX()":     "MAX requires at least 1 argument",
 		"=MAX(NA())": "#N/A",
@@ -1355,6 +1378,11 @@ func TestCalcCellValue(t *testing.T) {
 		"=PERMUT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax",
 		"=PERMUT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
 		"=PERMUT(6,8)":    "#N/A",
+		// SMALL
+		"=SMALL()":           "SMALL requires 2 arguments",
+		"=SMALL(A1:A5,0)":    "k should be > 0",
+		"=SMALL(A1:A5,6)":    "k should be <= length of array",
+		"=SMALL(A1:A5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
 		// Information Functions
 		// ISBLANK
 		"=ISBLANK(A1,A2)": "ISBLANK requires 1 argument",
@@ -1494,6 +1522,10 @@ func TestCalcCellValue(t *testing.T) {
 		"=RIGHTB(\"\",2,3)":  "RIGHTB allows at most 2 arguments",
 		"=RIGHTB(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
 		"=RIGHTB(\"\",-1)":   "#VALUE!",
+		// SUBSTITUTE
+		"=SUBSTITUTE()":                    "SUBSTITUTE requires 3 or 4 arguments",
+		"=SUBSTITUTE(\"\",\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
+		"=SUBSTITUTE(\"\",\"\",\"\",0)":    "instance_num should be > 0",
 		// TRIM
 		"=TRIM()":    "TRIM requires 1 argument",
 		"=TRIM(1,2)": "TRIM requires 1 argument",