451 lines
9.1 KiB
Go
451 lines
9.1 KiB
Go
|
package govaluate
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"time"
|
||
|
"unicode"
|
||
|
)
|
||
|
|
||
|
func parseTokens(expression string, functions map[string]ExpressionFunction) ([]ExpressionToken, error) {
|
||
|
|
||
|
var ret []ExpressionToken
|
||
|
var token ExpressionToken
|
||
|
var stream *lexerStream
|
||
|
var state lexerState
|
||
|
var err error
|
||
|
var found bool
|
||
|
|
||
|
stream = newLexerStream(expression)
|
||
|
state = validLexerStates[0]
|
||
|
|
||
|
for stream.canRead() {
|
||
|
|
||
|
token, err, found = readToken(stream, state, functions)
|
||
|
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
|
||
|
if !found {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
state, err = getLexerStateForToken(token.Kind)
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
|
||
|
// append this valid token
|
||
|
ret = append(ret, token)
|
||
|
}
|
||
|
|
||
|
err = checkBalance(ret)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return ret, nil
|
||
|
}
|
||
|
|
||
|
func readToken(stream *lexerStream, state lexerState, functions map[string]ExpressionFunction) (ExpressionToken, error, bool) {
|
||
|
|
||
|
var function ExpressionFunction
|
||
|
var ret ExpressionToken
|
||
|
var tokenValue interface{}
|
||
|
var tokenTime time.Time
|
||
|
var tokenString string
|
||
|
var kind TokenKind
|
||
|
var character rune
|
||
|
var found bool
|
||
|
var completed bool
|
||
|
var err error
|
||
|
|
||
|
// numeric is 0-9, or .
|
||
|
// string starts with '
|
||
|
// variable is alphanumeric, always starts with a letter
|
||
|
// bracket always means variable
|
||
|
// symbols are anything non-alphanumeric
|
||
|
// all others read into a buffer until they reach the end of the stream
|
||
|
for stream.canRead() {
|
||
|
|
||
|
character = stream.readCharacter()
|
||
|
|
||
|
if unicode.IsSpace(character) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
kind = UNKNOWN
|
||
|
|
||
|
// numeric constant
|
||
|
if isNumeric(character) {
|
||
|
|
||
|
tokenString = readTokenUntilFalse(stream, isNumeric)
|
||
|
tokenValue, err = strconv.ParseFloat(tokenString, 64)
|
||
|
|
||
|
if err != nil {
|
||
|
errorMsg := fmt.Sprintf("Unable to parse numeric value '%v' to float64\n", tokenString)
|
||
|
return ExpressionToken{}, errors.New(errorMsg), false
|
||
|
}
|
||
|
kind = NUMERIC
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// comma, separator
|
||
|
if character == ',' {
|
||
|
|
||
|
tokenValue = ","
|
||
|
kind = SEPARATOR
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// escaped variable
|
||
|
if character == '[' {
|
||
|
|
||
|
tokenValue, completed = readUntilFalse(stream, true, false, true, isNotClosingBracket)
|
||
|
kind = VARIABLE
|
||
|
|
||
|
if !completed {
|
||
|
return ExpressionToken{}, errors.New("Unclosed parameter bracket"), false
|
||
|
}
|
||
|
|
||
|
// above method normally rewinds us to the closing bracket, which we want to skip.
|
||
|
stream.rewind(-1)
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// regular variable - or function?
|
||
|
if unicode.IsLetter(character) {
|
||
|
|
||
|
tokenString = readTokenUntilFalse(stream, isVariableName)
|
||
|
|
||
|
tokenValue = tokenString
|
||
|
kind = VARIABLE
|
||
|
|
||
|
// boolean?
|
||
|
if tokenValue == "true" {
|
||
|
|
||
|
kind = BOOLEAN
|
||
|
tokenValue = true
|
||
|
} else {
|
||
|
|
||
|
if tokenValue == "false" {
|
||
|
|
||
|
kind = BOOLEAN
|
||
|
tokenValue = false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// textual operator?
|
||
|
if tokenValue == "in" || tokenValue == "IN" {
|
||
|
|
||
|
// force lower case for consistency
|
||
|
tokenValue = "in"
|
||
|
kind = COMPARATOR
|
||
|
}
|
||
|
|
||
|
// function?
|
||
|
function, found = functions[tokenString]
|
||
|
if found {
|
||
|
kind = FUNCTION
|
||
|
tokenValue = function
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if !isNotQuote(character) {
|
||
|
tokenValue, completed = readUntilFalse(stream, true, false, true, isNotQuote)
|
||
|
|
||
|
if !completed {
|
||
|
return ExpressionToken{}, errors.New("Unclosed string literal"), false
|
||
|
}
|
||
|
|
||
|
// advance the stream one position, since reading until false assumes the terminator is a real token
|
||
|
stream.rewind(-1)
|
||
|
|
||
|
// check to see if this can be parsed as a time.
|
||
|
tokenTime, found = tryParseTime(tokenValue.(string))
|
||
|
if found {
|
||
|
kind = TIME
|
||
|
tokenValue = tokenTime
|
||
|
} else {
|
||
|
kind = STRING
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if character == '(' {
|
||
|
tokenValue = character
|
||
|
kind = CLAUSE
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if character == ')' {
|
||
|
tokenValue = character
|
||
|
kind = CLAUSE_CLOSE
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// must be a known symbol
|
||
|
tokenString = readTokenUntilFalse(stream, isNotAlphanumeric)
|
||
|
tokenValue = tokenString
|
||
|
|
||
|
// quick hack for the case where "-" can mean "prefixed negation" or "minus", which are used
|
||
|
// very differently.
|
||
|
if state.canTransitionTo(PREFIX) {
|
||
|
_, found = prefixSymbols[tokenString]
|
||
|
if found {
|
||
|
|
||
|
kind = PREFIX
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
_, found = modifierSymbols[tokenString]
|
||
|
if found {
|
||
|
|
||
|
kind = MODIFIER
|
||
|
break
|
||
|
}
|
||
|
|
||
|
_, found = logicalSymbols[tokenString]
|
||
|
if found {
|
||
|
|
||
|
kind = LOGICALOP
|
||
|
break
|
||
|
}
|
||
|
|
||
|
_, found = comparatorSymbols[tokenString]
|
||
|
if found {
|
||
|
|
||
|
kind = COMPARATOR
|
||
|
break
|
||
|
}
|
||
|
|
||
|
_, found = ternarySymbols[tokenString]
|
||
|
if found {
|
||
|
|
||
|
kind = TERNARY
|
||
|
break
|
||
|
}
|
||
|
|
||
|
errorMessage := fmt.Sprintf("Invalid token: '%s'", tokenString)
|
||
|
return ret, errors.New(errorMessage), false
|
||
|
}
|
||
|
|
||
|
ret.Kind = kind
|
||
|
ret.Value = tokenValue
|
||
|
|
||
|
return ret, nil, (kind != UNKNOWN)
|
||
|
}
|
||
|
|
||
|
func readTokenUntilFalse(stream *lexerStream, condition func(rune) bool) string {
|
||
|
|
||
|
var ret string
|
||
|
|
||
|
stream.rewind(1)
|
||
|
ret, _ = readUntilFalse(stream, false, true, true, condition)
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Returns the string that was read until the given [condition] was false, or whitespace was broken.
|
||
|
Returns false if the stream ended before whitespace was broken or condition was met.
|
||
|
*/
|
||
|
func readUntilFalse(stream *lexerStream, includeWhitespace bool, breakWhitespace bool, allowEscaping bool, condition func(rune) bool) (string, bool) {
|
||
|
|
||
|
var tokenBuffer bytes.Buffer
|
||
|
var character rune
|
||
|
var conditioned bool
|
||
|
|
||
|
conditioned = false
|
||
|
|
||
|
for stream.canRead() {
|
||
|
|
||
|
character = stream.readCharacter()
|
||
|
|
||
|
// Use backslashes to escape anything
|
||
|
if allowEscaping && character == '\\' {
|
||
|
|
||
|
character = stream.readCharacter()
|
||
|
tokenBuffer.WriteString(string(character))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if unicode.IsSpace(character) {
|
||
|
|
||
|
if breakWhitespace && tokenBuffer.Len() > 0 {
|
||
|
conditioned = true
|
||
|
break
|
||
|
}
|
||
|
if !includeWhitespace {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if condition(character) {
|
||
|
tokenBuffer.WriteString(string(character))
|
||
|
} else {
|
||
|
conditioned = true
|
||
|
stream.rewind(1)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return tokenBuffer.String(), conditioned
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Checks to see if any optimizations can be performed on the given [tokens], which form a complete, valid expression.
|
||
|
The returns slice will represent the optimized (or unmodified) list of tokens to use.
|
||
|
*/
|
||
|
func optimizeTokens(tokens []ExpressionToken) ([]ExpressionToken, error) {
|
||
|
|
||
|
var token ExpressionToken
|
||
|
var symbol OperatorSymbol
|
||
|
var err error
|
||
|
var index int
|
||
|
|
||
|
for index, token = range tokens {
|
||
|
|
||
|
// if we find a regex operator, and the right-hand value is a constant, precompile and replace with a pattern.
|
||
|
if token.Kind != COMPARATOR {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
symbol = comparatorSymbols[token.Value.(string)]
|
||
|
if symbol != REQ && symbol != NREQ {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
index++
|
||
|
token = tokens[index]
|
||
|
if token.Kind == STRING {
|
||
|
|
||
|
token.Kind = PATTERN
|
||
|
token.Value, err = regexp.Compile(token.Value.(string))
|
||
|
|
||
|
if err != nil {
|
||
|
return tokens, err
|
||
|
}
|
||
|
|
||
|
tokens[index] = token
|
||
|
}
|
||
|
}
|
||
|
return tokens, nil
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Checks the balance of tokens which have multiple parts, such as parenthesis.
|
||
|
*/
|
||
|
func checkBalance(tokens []ExpressionToken) error {
|
||
|
|
||
|
var stream *tokenStream
|
||
|
var token ExpressionToken
|
||
|
var parens int
|
||
|
|
||
|
stream = newTokenStream(tokens)
|
||
|
|
||
|
for stream.hasNext() {
|
||
|
|
||
|
token = stream.next()
|
||
|
if token.Kind == CLAUSE {
|
||
|
parens++
|
||
|
continue
|
||
|
}
|
||
|
if token.Kind == CLAUSE_CLOSE {
|
||
|
parens--
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if parens != 0 {
|
||
|
return errors.New("Unbalanced parenthesis")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func isNumeric(character rune) bool {
|
||
|
|
||
|
return unicode.IsDigit(character) || character == '.'
|
||
|
}
|
||
|
|
||
|
func isNotQuote(character rune) bool {
|
||
|
|
||
|
return character != '\'' && character != '"'
|
||
|
}
|
||
|
|
||
|
func isNotAlphanumeric(character rune) bool {
|
||
|
|
||
|
return !(unicode.IsDigit(character) ||
|
||
|
unicode.IsLetter(character) ||
|
||
|
character == '(' ||
|
||
|
character == ')' ||
|
||
|
!isNotQuote(character))
|
||
|
}
|
||
|
|
||
|
func isVariableName(character rune) bool {
|
||
|
|
||
|
return unicode.IsLetter(character) ||
|
||
|
unicode.IsDigit(character) ||
|
||
|
character == '_'
|
||
|
}
|
||
|
|
||
|
func isNotClosingBracket(character rune) bool {
|
||
|
|
||
|
return character != ']'
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Attempts to parse the [candidate] as a Time.
|
||
|
Tries a series of standardized date formats, returns the Time if one applies,
|
||
|
otherwise returns false through the second return.
|
||
|
*/
|
||
|
func tryParseTime(candidate string) (time.Time, bool) {
|
||
|
|
||
|
var ret time.Time
|
||
|
var found bool
|
||
|
|
||
|
timeFormats := [...]string{
|
||
|
time.ANSIC,
|
||
|
time.UnixDate,
|
||
|
time.RubyDate,
|
||
|
time.Kitchen,
|
||
|
time.RFC3339,
|
||
|
time.RFC3339Nano,
|
||
|
"2006-01-02", // RFC 3339
|
||
|
"2006-01-02 15:04", // RFC 3339 with minutes
|
||
|
"2006-01-02 15:04:05", // RFC 3339 with seconds
|
||
|
"2006-01-02 15:04:05-07:00", // RFC 3339 with seconds and timezone
|
||
|
"2006-01-02T15Z0700", // ISO8601 with hour
|
||
|
"2006-01-02T15:04Z0700", // ISO8601 with minutes
|
||
|
"2006-01-02T15:04:05Z0700", // ISO8601 with seconds
|
||
|
"2006-01-02T15:04:05.999999999Z0700", // ISO8601 with nanoseconds
|
||
|
}
|
||
|
|
||
|
for _, format := range timeFormats {
|
||
|
|
||
|
ret, found = tryParseExactTime(candidate, format)
|
||
|
if found {
|
||
|
return ret, true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return time.Now(), false
|
||
|
}
|
||
|
|
||
|
func tryParseExactTime(candidate string, format string) (time.Time, bool) {
|
||
|
|
||
|
var ret time.Time
|
||
|
var err error
|
||
|
|
||
|
ret, err = time.ParseInLocation(format, candidate, time.Local)
|
||
|
if err != nil {
|
||
|
return time.Now(), false
|
||
|
}
|
||
|
|
||
|
return ret, true
|
||
|
}
|