Skip to content

Commit

Permalink
- formula engine: reduce cyclomatic complexity
Browse files Browse the repository at this point in the history
- styles: allow empty and default cell formats, qax-os#628
  • Loading branch information
xuri committed May 10, 2020
1 parent 4188dc7 commit 882abb8
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 154 deletions.
303 changes: 176 additions & 127 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ type formulaArg struct {
type formulaFuncs struct{}

// CalcCellValue provides a function to get calculated cell value. This
// feature is currently in beta. Array formula, table formula and some other
// formulas are not supported currently.
// feature is currently in working processing. Array formula, table formula
// and some other formulas are not supported currently.
func (f *File) CalcCellValue(sheet, cell string) (result string, err error) {
var (
formula string
Expand Down Expand Up @@ -265,6 +265,89 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error)
return opdStack.Peek().(efp.Token), err
}

// calcAdd evaluate addition arithmetic operations.
func calcAdd(opdStack *Stack) error {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal + rOpdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
return nil
}

// calcAdd evaluate subtraction arithmetic operations.
func calcSubtract(opdStack *Stack) error {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal - rOpdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
return nil
}

// calcAdd evaluate multiplication arithmetic operations.
func calcMultiply(opdStack *Stack) error {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal * rOpdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
return nil
}

// calcAdd evaluate division arithmetic operations.
func calcDivide(opdStack *Stack) error {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal / rOpdVal
if rOpdVal == 0 {
return errors.New(formulaErrorDIV)
}
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
return nil
}

// calculate evaluate basic arithmetic operations.
func calculate(opdStack *Stack, opt efp.Token) error {
if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix {
Expand All @@ -279,80 +362,69 @@ func calculate(opdStack *Stack, opt efp.Token) error {
result := 0 - opdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}

if opt.TValue == "+" {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
if err := calcAdd(opdStack); err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal + rOpdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}
if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
if err := calcSubtract(opdStack); err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal - rOpdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}
if opt.TValue == "*" {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
if err := calcMultiply(opdStack); err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal * rOpdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}
if opt.TValue == "/" {
if opdStack.Len() < 2 {
return errors.New("formula not valid")
}
rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token)
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
if err != nil {
if err := calcDivide(opdStack); err != nil {
return err
}
rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
if err != nil {
return err
}
result := lOpdVal / rOpdVal
if rOpdVal == 0 {
return errors.New(formulaErrorDIV)
}
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}
return nil
}

// parseOperatorPrefixToken parse operator prefix token.
func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Token) (err error) {
if optStack.Len() == 0 {
optStack.Push(token)
} else {
tokenPriority := getPriority(token)
topOpt := optStack.Peek().(efp.Token)
topOptPriority := getPriority(topOpt)
if tokenPriority > topOptPriority {
optStack.Push(token)
} else {
for tokenPriority <= topOptPriority {
optStack.Pop()
if err = calculate(opdStack, topOpt); err != nil {
return
}
if optStack.Len() > 0 {
topOpt = optStack.Peek().(efp.Token)
topOptPriority = getPriority(topOpt)
continue
}
break
}
optStack.Push(token)
}
}
return
}

// isOperatorPrefixToken determine if the token is parse operator prefix
// token.
func isOperatorPrefixToken(token efp.Token) bool {
if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) ||
token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" {
return true
}
return false
}

// parseToken parse basic arithmetic operator priority and evaluate based on
// operators and operands.
func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error {
Expand All @@ -369,30 +441,9 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta
token.TType = efp.TokenTypeOperand
token.TSubType = efp.TokenSubTypeNumber
}
if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" {
if optStack.Len() == 0 {
optStack.Push(token)
} else {
tokenPriority := getPriority(token)
topOpt := optStack.Peek().(efp.Token)
topOptPriority := getPriority(topOpt)
if tokenPriority > topOptPriority {
optStack.Push(token)
} else {
for tokenPriority <= topOptPriority {
optStack.Pop()
if err := calculate(opdStack, topOpt); err != nil {
return err
}
if optStack.Len() > 0 {
topOpt = optStack.Peek().(efp.Token)
topOptPriority = getPriority(topOpt)
continue
}
break
}
optStack.Push(token)
}
if isOperatorPrefixToken(token) {
if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil {
return err
}
}
if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // (
Expand Down Expand Up @@ -461,11 +512,44 @@ func (f *File) parseReference(sheet, reference string) (result []string, matrix
return
}

// prepareValueRange prepare value range.
func prepareValueRange(cr cellRange, valueRange []int) {
if cr.From.Row < valueRange[0] {
valueRange[0] = cr.From.Row
}
if cr.From.Col < valueRange[2] {
valueRange[2] = cr.From.Col
}
if cr.To.Row > valueRange[0] {
valueRange[1] = cr.To.Row
}
if cr.To.Col > valueRange[3] {
valueRange[3] = cr.To.Col
}
}

// prepareValueRef prepare value reference.
func prepareValueRef(cr cellRef, valueRange []int) {
if cr.Row < valueRange[0] {
valueRange[0] = cr.Row
}
if cr.Col < valueRange[2] {
valueRange[2] = cr.Col
}
if cr.Row > valueRange[0] {
valueRange[1] = cr.Row
}
if cr.Col > valueRange[3] {
valueRange[3] = cr.Col
}
}

// rangeResolver extract value as string from given reference and range list.
// This function will not ignore the empty cell. For example,
// A1:A2:A2:B3 will be reference A1:B3.
// This function will not ignore the empty cell. For example, A1:A2:A2:B3 will
// be reference A1:B3.
func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, matrix [][]string, err error) {
var fromRow, toRow, fromCol, toCol int = 1, 1, 1, 1
// value range order: from row, to row, from column, to column
valueRange := []int{1, 1, 1, 1}
var sheet string
filter := map[string]string{}
// prepare value range
Expand All @@ -476,18 +560,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string,
}
rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row}
sortCoordinates(rng)
if cr.From.Row < fromRow {
fromRow = cr.From.Row
}
if cr.From.Col < fromCol {
fromCol = cr.From.Col
}
if cr.To.Row > fromRow {
toRow = cr.To.Row
}
if cr.To.Col > toCol {
toCol = cr.To.Col
}
prepareValueRange(cr, valueRange)
if cr.From.Sheet != "" {
sheet = cr.From.Sheet
}
Expand All @@ -497,24 +570,13 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string,
if cr.Sheet != "" {
sheet = cr.Sheet
}
if cr.Row < fromRow {
fromRow = cr.Row
}
if cr.Col < fromCol {
fromCol = cr.Col
}
if cr.Row > fromRow {
toRow = cr.Row
}
if cr.Col > toCol {
toCol = cr.Col
}
prepareValueRef(cr, valueRange)
}
// extract value from ranges
if cellRanges.Len() > 0 {
for row := fromRow; row <= toRow; row++ {
for row := valueRange[0]; row <= valueRange[1]; row++ {
var matrixRow = []string{}
for col := fromCol; col <= toCol; col++ {
for col := valueRange[2]; col <= valueRange[3]; col++ {
var cell, value string
if cell, err = CoordinatesToCellName(col, row); err != nil {
return
Expand Down Expand Up @@ -672,28 +734,15 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) {
err = errors.New("ARABIC requires 1 numeric argument")
return
}
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).Value {
digit := 0.0
switch char {
case '-':
if char == '-' {
prefix = -1
continue
case 'I':
digit = 1
case 'V':
digit = 5
case 'X':
digit = 10
case 'L':
digit = 50
case 'C':
digit = 100
case 'D':
digit = 500
case 'M':
digit = 1000
}
digit, _ = charMap[char]
val += digit
switch {
case last == digit && (last == 5 || last == 50 || last == 500):
Expand Down Expand Up @@ -850,7 +899,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) {
return
}
if radix < 2 || radix > 36 {
err = errors.New("radix must be an integer 2 and 36")
err = errors.New("radix must be an integer >= 2 and <= 36")
return
}
if argsList.Len() > 2 {
Expand Down
Loading

0 comments on commit 882abb8

Please sign in to comment.