From 256914441f5da89b5486136f20fbafe1617bdd25 Mon Sep 17 00:00:00 2001 From: Seb C Date: Sun, 5 May 2024 23:58:11 +0100 Subject: [PATCH] feat: Add True / False Bools + fix bugs, TODOs, new Stringer (#11) Add True / False Bools + fix bugs, TODOs, new Stringer --- .golangci.yml | 823 +++++++++++++++++++++++++++++++++++++++++++ README.md | 14 + function.go | 2 +- function_test.go | 1 + gal.go | 4 + gal_test.go | 14 +- tree.go | 74 +++- tree_builder.go | 64 +++- tree_builder_test.go | 3 +- tree_test.go | 3 +- value.go | 78 ++-- value_test.go | 12 + variable.go | 2 +- 13 files changed, 1048 insertions(+), 46 deletions(-) create mode 100644 .golangci.yml create mode 100644 value_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..961a584 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,823 @@ +# This file contains all available configuration options +# with their default values (in comments). +# +# This file is not a configuration example, +# it contains the exhaustive configuration with explanations of the options. + +# Options for analysis running. +run: + # The default concurrency value is the number of available CPU. + concurrency: 4 + + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 5m + + # Exit code when at least one issue was found. + # Default: 1 + issues-exit-code: 2 + + # Include test files or not. + # Default: true + # tests: false + + # List of build tags, all linters use it. + # Default: []. + # build-tags: + # - mytag + + # Which dirs to skip: issues from them won't be reported. + # Can use regexp here: `generated.*`, regexp is applied on full path. + # Default value is empty list, + # but default dirs are skipped independently of this option's value (see skip-dirs-use-default). + # "/" will be replaced by current OS file path separator to properly work on Windows. + skip-dirs: + # - src/external_libs + # - autogenerated_by_my_lib + + # Enables skipping of directories: + # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + # Default: true + # skip-dirs-use-default: false + + # Which files to skip: they will be analyzed, but issues from them won't be reported. + # Default value is empty list, + # but there is no need to include all autogenerated files, + # we confidently recognize autogenerated files. + # If it's not please let us know. + # "/" will be replaced by current OS file path separator to properly work on Windows. + # skip-files: + # - ".*\\.my\\.go$" + # - lib/bad.go + + # If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + # + # Allowed values: readonly|vendor|mod + # By default, it isn't set. + modules-download-mode: readonly + + # Allow multiple parallel golangci-lint instances running. + # If false (default) - golangci-lint acquires file lock on start. + allow-parallel-runners: false + + # Define the Go version limit. + # Mainly related to generics support in go1.18. + # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.17 + go: '1.17' + + +# output configuration options +output: + # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions + # + # Multiple can be specified by separating them by comma, output can be provided + # for each of them by separating format name and path by colon symbol. + # Output path can be either `stdout`, `stderr` or path to the file to write to. + # Example: "checkstyle:report.json,colored-line-number" + # + # Default: colored-line-number + # format: json + + # Print lines of code with issue. + # Default: true + # print-issued-lines: false + + # Print linter name in the end of issue text. + # Default: true + # print-linter-name: false + + # Make issues output unique by line. + # Default: true + # uniq-by-line: false + + # Add a prefix to the output file references. + # Default is no prefix. + path-prefix: "" + + # Sort results by: filepath, line and column. + # sort-results: false + + +# All available settings of specific linters. +linters-settings: + asasalint: + # To specify a set of function names to exclude. + # The values are merged with the builtin exclusions. + # The builtin exclusions can be disabled by setting `use-builtin-exclusions` to `false`. + # Default: ["^(fmt|log|logger|t|)\.(Print|Fprint|Sprint|Fatal|Panic|Error|Warn|Warning|Info|Debug|Log)(|f|ln)$"] + # exclude: + # - Append + # - \.Wrapf + # To enable/disable the asasalint builtin exclusions of function names. + # See the default value of `exclude` to get the builtin exclusions. + # Default: true + # use-builtin-exclusions: false + # Ignore *_test.go files. + # Default: false + # ignore-test: true + + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 15 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 3.5 + # Should ignore tests. + # Default: false + skip-tests: true + + dogsled: + # Checks assignments with too many blank identifiers. + # Default: 2 + max-blank-identifiers: 3 + + dupl: + # Tokens count to trigger issue. + # Default: 150 + # threshold: 100 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. + # Such cases aren't reported by default. + # Default: false + check-blank: true + + # To disable the errcheck built-in exclude list. + # See `-excludeonly` option in https://github.com/kisielk/errcheck#excluding-functions for details. + # Default: false + disable-default-exclusions: true + + # List of functions to exclude from checking, where each entry is a single function to exclude. + # See https://github.com/kisielk/errcheck#excluding-functions for details. + #exclude-functions: + # - io/ioutil.ReadFile + # - io.Copy(*bytes.Buffer) + # - io.Copy(os.Stdout) + + errchkjson: + # With check-error-free-encoding set to true, errchkjson does warn about errors + # from json encoding functions that are safe to be ignored, + # because they are not possible to happen. + # + # if check-error-free-encoding is set to true and errcheck linter is enabled, + # it is recommended to add the following exceptions to prevent from false positives: + # + # linters-settings: + # errcheck: + # exclude-functions: + # - encoding/json.Marshal + # - encoding/json.MarshalIndent + # + # Default: false + check-error-free-encoding: true + + # Issue on struct encoding that doesn't have exported fields. + # Default: false + report-no-exported: true + + gci: + # Section configuration to compare against. + # Section names are case-insensitive and may contain parameters in (). + # The order of sections is always `standard > default > custom`, + # it cannot be changed and doesn't follow the order of `sections` option. + # Default: ["standard", "default"] + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/seborama) # Custom section: groups all imports with the specified Prefix. + + # Skip generated files. + # Default: true + #skip-generated: false + + gocognit: + # Minimal code complexity to report + # Default: 30 (but we recommend 10-20) + min-complexity: 15 + + gocritic: + # Which checks should be enabled; can't be combined with 'disabled-checks'. + # See https://go-critic.github.io/overview#checks-overview. + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`. + # By default, list of stable checks is used. + enabled-checks: + - hugeParam + - nestingReduce + - rangeExprCopy + - rangeValCopy + - ruleguard + - tooManyResultsChecker + - truncateCmp + - unnamedResult + + + # Which checks should be disabled; can't be combined with 'enabled-checks'. + # Default: [] + disabled-checks: + + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # See https://github.com/go-critic/go-critic#usage -> section "Tags". + # Default: [] + enabled-tags: + # - diagnostic + # - style + # - performance + # - experimental + # - opinionated + disabled-tags: + # - diagnostic + # - style + # - performance + # - experimental + # - opinionated + + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + # Must be valid enabled check name. + captLocal: + # Whether to restrict checker to params only. + # Default: true + # paramsOnly: false + elseif: + # Whether to skip balanced if-else pairs. + # Default: true + # skipBalanced: false + hugeParam: + # Size in bytes that makes the warning trigger. + # Default: 80 + # sizeThreshold: 70 + nestingReduce: + # Min number of statements inside a branch to trigger a warning. + # Default: 5 + # bodyWidth: 4 + rangeExprCopy: + # Size in bytes that makes the warning trigger. + # Default: 512 + # sizeThreshold: 516 + # Whether to check test functions + # Default: true + skipTestFuncs: false + rangeValCopy: + # Size in bytes that makes the warning trigger. + # Default: 128 + # sizeThreshold: 32 + # Whether to check test functions. + # Default: true + skipTestFuncs: false + tooManyResultsChecker: + # Maximum number of results. + # Default: 5 + maxResults: 10 + truncateCmp: + # Whether to skip int/uint/uintptr types. + # Default: true + skipArchDependent: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + unnamedResult: + # Whether to check exported functions. + # Default: false + checkExported: true + + gocyclo: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 15 + + godot: + # Comments to be checked: `declarations`, `toplevel`, or `all`. + # Default: declarations + scope: all + # List of regexps for excluding particular comment lines from check. + # Default: [] + exclude: + # Exclude todo and fixme comments. + - "^fixme:" + - "^todo:" + # Check that each sentence ends with a period. + # Default: true + period: false + # Check that each sentence starts with a capital letter. + # Default: false + # capital: true + + gofmt: + # Simplify code: gofmt with `-s` option. + # Default: true + # simplify: false + + gofumpt: + # Select the Go version to target. + # Default: "1.15" + # Deprecated: use the global `run.go` instead. + #lang-version: "1.17" + + # Module path which contains the source code being formatted. + # Default: "" + # module-path: github.com/org/project + + # Choose whether to use the extra rules. + # Default: false + extra-rules: true + + goimports: + # Put imports beginning with prefix after 3rd-party packages. + # It's a comma-separated list of prefixes. + # Default: "" + local-prefixes: github.com/seborama + + gosimple: + # https://staticcheck.io/docs/options#checks + # Default: ["*"] + checks: [ "all" ] + + gosec: + # To select a subset of rules to run. + # Available rules: https://github.com/securego/gosec#available-rules + # Default: [] - means include all rules + # includes: + + # To specify a set of rules to explicitly exclude. + # Available rules: https://github.com/securego/gosec#available-rules + # Default: [] + # excludes: + + # Exclude generated files + # Default: false + exclude-generated: true + + # Filter out the issues with a lower severity than the given value. + # Valid options are: low, medium, high. + # Default: low + severity: medium + + # Filter out the issues with a lower confidence than the given value. + # Valid options are: low, medium, high. + # Default: low + confidence: medium + + # Concurrency value. + # Default: the number of logical CPUs usable by the current process. + # concurrency: 12 + + # To specify the configuration of rules. + config: + # Globals are applicable to all rules. + global: + # If true, ignore #nosec in comments (and an alternative as well). + # Default: false + nosec: true + # Add an alternative comment prefix to #nosec (both will work at the same time). + # Default: "" + "#nosec": "#my-custom-nosec" + # Define whether nosec issues are counted as finding or not. + # Default: false + show-ignored: true + # Audit mode enables addition checks that for normal code analysis might be too nosy. + # Default: false + audit: true + G101: + # Regexp pattern for variables and constants to find. + # Default: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" + pattern: "(?i)example" + # If true, complain about all cases (even with low entropy). + # Default: false + ignore_entropy: false + # Maximum allowed entropy of the string. + # Default: "80.0" + entropy_threshold: "80.0" + # Maximum allowed value of entropy/string length. + # Is taken into account if entropy >= entropy_threshold/2. + # Default: "3.0" + per_char_threshold: "3.0" + # Calculate entropy for first N chars of the string. + # Default: "16" + truncate: "32" + # Additional functions to ignore while checking unhandled errors. + # Following functions always ignored: + # bytes.Buffer: + # - Write + # - WriteByte + # - WriteRune + # - WriteString + # fmt: + # - Print + # - Printf + # - Println + # - Fprint + # - Fprintf + # - Fprintln + # strings.Builder: + # - Write + # - WriteByte + # - WriteRune + # - WriteString + # io.PipeWriter: + # - CloseWithError + # hash.Hash: + # - Write + # os: + # - Unsetenv + # Default: {} + G104: + fmt: + - Fscanf + G111: + # Regexp pattern to find potential directory traversal. + # Default: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" + pattern: "custom\\.Dir\\(\\)" + # Maximum allowed permissions mode for os.Mkdir and os.MkdirAll + # Default: "0750" + G301: "0750" + # Maximum allowed permissions mode for os.OpenFile and os.Chmod + # Default: "0600" + G302: "0600" + # Maximum allowed permissions mode for os.WriteFile and ioutil.WriteFile + # Default: "0600" + G306: "0600" + + govet: + # Report about shadowed variables. + # Default: false + check-shadowing: false + + # Settings per analyzer. + settings: + # Analyzer name, run `go tool vet help` to see all analyzers. + + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + # - asmdecl + # - assign + # - bools + # - buildtag + # - cgocall + # - composites + # - deepequalerrors + # - errorsas + # - findcall + # - framepointer + # - httpresponse + # - ifaceassert + # - nilfunc + # - nilness + # - printf + # - reflectvaluecompare + # - shift + # - sigchanyzer + # - sortslice + # - stdmethods + # - stringintconv + # - testinggoroutine + # - tests + # - unmarshal + # - unreachable + # - unsafeptr + # - unusedresult + # - unusedwrite + + ireturn: + # ireturn allows using `allow` and `reject` settings at the same time. + # Both settings are lists of the keywords and regular expressions matched to interface or package names. + # keywords: + # - `empty` for `interface{}` + # - `error` for errors + # - `stdlib` for standard library + # - `anon` for anonymous interfaces + + # By default, it allows using errors, empty interfaces, anonymous interfaces, + # and interfaces provided by the standard library. + allow: + - anon + - error + - empty + - stdlib + # You can specify idiomatic endings for interface + - (or|er)$ + + # reject-list of interfaces + # reject: + # - github.com\/user\/package\/v4\.Type + + maintidx: + # Show functions with maintainability index lower than N. + # A high index indicates better maintainability (it's kind of the opposite of complexity). + # Default: 20 + # under: 100 + + nestif: + # Minimal complexity of if statements to report. + # Default: 5 + min-complexity: 4 + + nilnil: + # Checks that there is no simultaneous return of `nil` error and an invalid value. + # Default: ["ptr", "func", "iface", "map", "chan"] + checked-types: + - ptr + - func + - iface + - map + - chan + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + # - github.com/jmoiron/sqlx + + staticcheck: + # Select the Go version to target. + # Default: "1.13" + # Deprecated: use the global `run.go` instead. + # go: "1.15" + # https://staticcheck.io/docs/options#checks + # Default: ["*"] + checks: [ "all" ] + + stylecheck: + # Select the Go version to target. + # Default: 1.13 + # Deprecated: use the global `run.go` instead. + # go: "1.15" + # https://staticcheck.io/docs/options#checks + # Default: ["*"] + # checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ] + checks: [ "all", "-ST1003", "-ST1016" ] + # https://staticcheck.io/docs/options#dot_import_whitelist + # Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"] + dot-import-whitelist: + - fmt + # https://staticcheck.io/docs/options#initialisms + # Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] + initialisms: [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS" ] + # https://staticcheck.io/docs/options#http_status_code_whitelist + # Default: ["200", "400", "404", "500"] + http-status-code-whitelist: [ "200", "400", "404", "500" ] + + tagliatelle: + # Check the struck tag name case. + case: + # Use the struct field name to check the name of the struct tag. + # Default: false + use-field-name: true + # `camel` is used for `json` and `yaml` (can be overridden) + # Default: {} + rules: + # Any struct tag type can be used. + # Support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` + json: camel + yaml: camel + xml: camel + bson: camel + avro: snake + mapstructure: kebab + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: false + + unparam: + # Inspect exported functions. + # + # Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + # + # Default: false + check-exported: true + +linters: + # Disable all linters. + # Default: false + disable-all: true + # Enable specific linter + # https://golangci-lint.run/usage/linters/#enabled-by-default-linters + enable: + # - asciicheck + # - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + # - decorder + # - depguard + - dogsled + - dupl + - durationcheck + - errcheck + - errchkjson + # - errname + # - errorlint + # - exhaustive + # - exhaustivestruct + - exportloopref + # - forbidigo + # - forcetypeassert + # - funlen + # - gci + # - gochecknoglobals + # - gochecknoinits + - gocognit + # - goconst + - gocritic + - gocyclo + - godot + # - godox + - goerr113 + - gofmt + - gofumpt + # - goheader + - goimports + #- gomnd + #- gomoddirectives + #- gomodguard + #- goprintffuncname + - gosec + - gosimple + - govet + #- grouper + #- importas + - ineffassign + #- interfacer + # - ireturn + #- lll + - maintidx + #- makezero + #- nakedret + - nestif + - nilerr + # - nilnil + #- nlreturn + #- noctx + # - nolintlint + #- paralleltest + #- prealloc + - predeclared + #- promlinter + # - revive + - rowserrcheck + #- scopelint + - sqlclosecheck + - staticcheck + - stylecheck + - tagliatelle + - tenv + # - testpackage + - thelper + # - tparallel + - typecheck + - unconvert + - unparam + - unused + # - varnamelen + - vetshadow + # - wastedassign + # - whitespace + # - wrapcheck + # - wsl + + # Run only fast linters from enabled linters set (first run won't be fast) + # Default: false + fast: false + + +issues: + # List of regexps of issue texts to exclude. + # + # But independently of this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. + # To list all excluded by default patterns execute `golangci-lint run --help` + # + # Default: [] + #exclude: + # - abcdef + + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - cyclop + - errcheck + - dupl + - gocognit + - gocyclo + - goerr113 + - gosec + + - path: _test\.go + text: "fieldalignment:" + + # Exclude known linters from partially hard-vendored code, + # which is impossible to exclude via `nolint` comments. + - path: internal/hmac/ + text: "weak cryptographic primitive" + linters: + - gosec + + # Exclude some `staticcheck` messages. + - linters: + - staticcheck + text: "SA9003:" + + # Exclude `lll` issues for long lines with `go:generate`. + - linters: + - lll + source: "^//go:generate " + + # Independently of option `exclude` we use default exclude patterns, + # it can be disabled by this option. + # To list all excluded by default patterns execute `golangci-lint run --help`. + # Default: true. + # exclude-use-default: false + + # If set to true exclude and exclude-rules regular expressions become case-sensitive. + # Default: false + exclude-case-sensitive: false + + # The list of ids of default excludes to include or disable. + # Default: [] + # include: + # - EXC0002 # disable excluding of issues about comments from golint. + + # Maximum issues count per one linter. + # Set to 0 to disable. + # Default: 50 + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 0 + + # Show only new issues: if there are unstaged changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing large codebase. + # It's not practical to fix all existing issues at the moment of integration: + # much better don't allow issues in new code. + # + # Default: false. + # new: true + + # Show only new issues created after git revision `REV`. + # new-from-rev: HEAD + + # Fix found issues (if it's supported by the linter). + # fix: true + + +severity: + # Set the default severity for issues. + # + # If severity rules are defined and the issues do not match or no severity is provided to the rule + # this will be the default severity applied. + # Severities should match the supported severity names of the selected out format. + # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity + # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity + # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + # + # Default value is an empty string. + # default-severity: error + default-severity: warning + + # If set to true `severity-rules` regular expressions become case-sensitive. + # Default: false + case-sensitive: false + + # When a list of severity rules are provided, severity information will be added to lint issues. + # Severity rules have the same filtering capability as exclude rules + # except you are allowed to specify one matcher per severity rule. + # Only affects out formats that support setting severity information. + # + # Default: [] + rules: + - linters: + - dupl + severity: info diff --git a/README.md b/README.md index da3d45e..a5d90d2 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,20 @@ Both examples will happily accept a `Value` of type `String` or `Number` and pro Numbers implement arbitrary precision fixed-point decimal arithmetic with [shopspring/decimal](https://github.com/shopspring/decimal). +## Strings + +Strings must be enclosed in double-quotes (`"`) e.g. valid: `"this is a string"`, invalid: `this is a syntax error` (missing double-quotes). + +Escapes are supported: +- `"this is \"also\" a valid string"` +- `"this is fine too\\"` (escapes cancel each other out) + +## Bools + +In additional to boolean expressions, sepcial contants `True` and `False` may be used. + +Do not double-quote them, or they will become plain strings! + ## Supported operations * Operators: `+` `-` `*` `/` `%` `**` `<<` `>>` `<` `<=` `==` `!=` `>` `>=` `And` `&&` `Or` `||` diff --git a/function.go b/function.go index 0f5422e..25dff13 100644 --- a/function.go +++ b/function.go @@ -103,7 +103,7 @@ func PiLong(args ...Value) Value { return NewUndefinedWithReasonf("pi() requires no argument, got %d", len(args)) } - pi, _ := NewNumberFromString(Pi51199) + pi, _ := NewNumberFromString(Pi51199) //nolint: errcheck return pi } diff --git a/function_test.go b/function_test.go index 305a8b5..d5afbfb 100644 --- a/function_test.go +++ b/function_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/seborama/gal/v8" + "github.com/stretchr/testify/assert" ) diff --git a/gal.go b/gal.go index 6282b57..192a5ee 100644 --- a/gal.go +++ b/gal.go @@ -1,5 +1,7 @@ package gal +import "fmt" + type exprType int const ( @@ -10,6 +12,7 @@ const ( stringType variableType functionType + boolType ) type Value interface { @@ -33,6 +36,7 @@ type Value interface { Or(Value) Bool // Helpers Stringer + fmt.Stringer entry } diff --git a/gal_test.go b/gal_test.go index 1776a8f..b5d856e 100644 --- a/gal_test.go +++ b/gal_test.go @@ -1,12 +1,12 @@ package gal_test import ( - "fmt" "testing" "github.com/google/go-cmp/cmp" - "github.com/seborama/gal/v8" "github.com/stretchr/testify/assert" + + "github.com/seborama/gal/v8" ) func TestEval(t *testing.T) { @@ -130,6 +130,10 @@ func TestEval_Boolean(t *testing.T) { expr = `( 123 == 123 And 12 > 45 ) Or ( "b" != "b" )` val = gal.Parse(expr).Eval() assert.Equal(t, gal.False.String(), val.String()) + + expr = `True Or False` + val = gal.Parse(expr).Eval() + assert.Equal(t, gal.True.String(), val.String()) } func TestWithVariablesAndFunctions(t *testing.T) { @@ -298,11 +302,9 @@ func TestMultiValueFunctions(t *testing.T) { // That way, it can receiv either two Numberer's or one single MultiValue that holds 2 Numberer's. var margs gal.MultiValue if len(args) == 1 { - fmt.Println("DEBUG - a single MultiValue") margs = args[0].(gal.MultiValue) // not checking type satisfaction for simplicity } if len(args) == 2 { - fmt.Println("DEBUG - two Value's") margs = gal.NewMultiValue(args...) } if margs.Size() != 2 { @@ -328,7 +330,7 @@ func TestStringsWithSpaces(t *testing.T) { parsedExpr := gal.Parse(expr) got := parsedExpr.Eval() - assert.Equal(t, "ab cdef gh", got.String()) + assert.Equal(t, `"ab cdef gh"`, got.String()) } func TestFunctionsAndStringsWithSpaces(t *testing.T) { @@ -345,5 +347,5 @@ func TestFunctionsAndStringsWithSpaces(t *testing.T) { }, }), ) - assert.Equal(t, "ab cdef gh", got.String()) + assert.Equal(t, `"ab cdef gh"`, got.String()) } diff --git a/tree.go b/tree.go index a7b4777..4be3046 100644 --- a/tree.go +++ b/tree.go @@ -1,7 +1,32 @@ package gal +import ( + "fmt" + "log/slog" + "strings" +) + type entryKind int +func (ek entryKind) String() string { + switch ek { + case unknownEntryKind: + return "unknownEntryKind" + case valueEntryKind: + return "valueEntryKind" + case operatorEntryKind: + return "operatorEntryKind" + case treeEntryKind: + return "treeEntryKind" + case functionEntryKind: + return "functionEntryKind" + case variableEntryKind: + return "variableEntryKind" + default: + return fmt.Sprintf("unknown:%d", ek) + } +} + const ( unknownEntryKind entryKind = iota valueEntryKind @@ -90,7 +115,7 @@ func WithFunctions(funcs Functions) treeOption { // It accepts optional functional parameters to supply user-defined // entities such as functions and variables. func (tree Tree) Eval(opts ...treeOption) Value { - //config + // config cfg := &treeConfig{} for _, o := range opts { @@ -141,15 +166,19 @@ func (tree Tree) Split() []Tree { // For instance, a tree representing the expression '2 + 5 * 4 / 2' with an operator precedence // of 'multiplicativeOperators' would read the Tree left to right and return a new Tree that // represents: '2 + 10' where 10 was calculated (and reduced) from 5 * 4 = 20 / 2 = 10. +// +// nolint: gocognit,gocyclo,cyclop func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *treeConfig) Tree { var outTree Tree var val entry - var op Operator = invalidOperator + var op Operator = invalidOperator //nolint: stylecheck for i := 0; i < tree.TrunkLen(); i++ { e := tree[i] + slog.Debug("Tree.Calc: entry in Tree", "i", i, "kind", e.kind().String()) if e == nil { + slog.Debug("Tree.Calc: nil entry in Tree") return Tree{ NewUndefinedWithReasonf("syntax error: nil value at tree entry #%d - tree: %+v", i, tree), } @@ -157,6 +186,7 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree switch e.kind() { case valueEntryKind: + slog.Debug("Tree.Calc: valueEntryKind", "i", i, "Value", e.(Value).String()) if val == nil && op == invalidOperator { val = e continue @@ -168,9 +198,12 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree } } + slog.Debug("Tree.Calc: valueEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "e", e.(Value).String()) val = calculate(val.(Value), op, e.(Value)) + slog.Debug("Tree.Calc: valueEntryKind - calculate", "i", i, "result", val.(Value).String()) case treeEntryKind: + slog.Debug("Tree.Calc: treeEntryKind", "i", i) if val == nil && op != invalidOperator { return Tree{ NewUndefinedWithReasonf("syntax error: missing left hand side value for operator '%s'", op.String()), @@ -184,9 +217,11 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree } val = calculate(val.(Value), op, rhsVal) + slog.Debug("Tree.Calc: treeEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "rhsVal", rhsVal.String(), "result", val.(Value).String()) case operatorEntryKind: - op = e.(Operator) + slog.Debug("Tree.Calc: operatorEntryKind", "i", i, "Value", e.(Operator).String()) + op = e.(Operator) //nolint: errcheck if isOperatorInPrecedenceGroup(op) { // same operator precedence: keep operating linearly, do not build a tree continue @@ -199,7 +234,8 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree op = invalidOperator case functionEntryKind: - f := e.(Function) + slog.Debug("Tree.Calc: functionEntryKind", "i", i, "name", e.(Function).Name) + f := e.(Function) //nolint: errcheck if f.BodyFn == nil { f.BodyFn = cfg.functions.Function(f.Name) } @@ -210,8 +246,10 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree } val = calculate(val.(Value), op, rhsVal) + slog.Debug("Tree.Calc: functionEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "rhsVal", rhsVal.String(), "result", val.(Value).String()) case variableEntryKind: + slog.Debug("Tree.Calc: variableEntryKind", "i", i, "name", e.(Variable).Name) varName := e.(Variable).Name rhsVal, ok := cfg.Variable(varName) if !ok { @@ -219,6 +257,7 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree NewUndefinedWithReasonf("syntax error: unknown variable name: '%s'", varName), } } + slog.Debug("Tree.Calc: variableEntryKind", "i", i, "value", rhsVal.String()) if val == nil { val = rhsVal @@ -226,6 +265,7 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree } val = calculate(val.(Value), op, rhsVal) + slog.Debug("Tree.Calc: variableEntryKind - calculate", "i", i, "val", val.(Value).String(), "op", op.String(), "rhsVal", rhsVal.String(), "result", val.(Value).String()) case unknownEntryKind: return Tree{e} @@ -274,6 +314,32 @@ func (Tree) kind() entryKind { return treeEntryKind } +func (tree Tree) String(indents ...string) string { + indent := strings.Join(indents, "") + + res := "" + for _, e := range tree { + switch e.kind() { + case unknownEntryKind: + res += fmt.Sprintf(indent+"unknownEntryKind %T\n", e) + case valueEntryKind: + res += fmt.Sprintf(indent+"Value %T %s\n", e, e.(Value).String()) + case operatorEntryKind: + res += fmt.Sprintf(indent+"Operator %s\n", e.(Operator).String()) + case treeEntryKind: + res += fmt.Sprintf(indent+"Tree {\n%s}\n", e.(Tree).String(" ")) + case functionEntryKind: + res += fmt.Sprintf(indent+"Function %s\n", e.(Function).Name) + case variableEntryKind: + res += fmt.Sprintf(indent+"Variable %s\n", e.(Variable).Name) + default: + res += fmt.Sprintf(indent+"undefined %T %s\n", e, e.kind().String()) + } + } + return res +} + +// nolint: gocognit,gocyclo,cyclop func calculate(lhs Value, op Operator, rhs Value) Value { var outVal Value diff --git a/tree_builder.go b/tree_builder.go index 59ff3f8..93611fa 100644 --- a/tree_builder.go +++ b/tree_builder.go @@ -12,6 +12,7 @@ func NewTreeBuilder() *TreeBuilder { return &TreeBuilder{} } +// nolint: gocognit,gocyclo,cyclop func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { tree := Tree{} @@ -33,6 +34,13 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { v := NewString(part) tree = append(tree, v) + case boolType: + v, err := NewBoolFromString(part) + if err != nil { + return nil, err + } + tree = append(tree, v) + case operatorType: switch part { case Plus.String(): @@ -76,7 +84,7 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { } case functionType: - fname, l, _ := readFunctionName(part) // ignore err: we already parsed the function name when in extractPart() + fname, l, _ := readFunctionName(part) //nolint: errcheck // ignore err: we already parsed the function name when in extractPart() v, err := tb.FromExpr(part[l+1 : len(part)-1]) // exclude leading '(' and trailing ')' if err != nil { return nil, err @@ -120,6 +128,8 @@ func (tb TreeBuilder) FromExpr(expr string) (Tree, error) { // returns the part extracted as string, the type extracted, the cursor position // after extraction or an error. +// +// nolint: gocognit,gocyclo,cyclop func extractPart(expr string) (string, exprType, int, error) { // left trim blanks pos := 0 @@ -144,6 +154,15 @@ func extractPart(expr string) (string, exprType, int, error) { return s, stringType, pos + l, nil } + // read part - "boolean" + { // make s, l and ok local scope + s, l, ok := readBool(expr[pos:]) + if ok { + // it's a bool + return s, boolType, pos + l, nil + } + } + // read part - :variable: if expr[pos] == ':' { s, l, err := readVariable(expr[pos:]) @@ -193,7 +212,7 @@ func readString(expr string) (string, int, error) { escapes := 0 for i, r := range expr[1:] { - to += 1 + to++ if expr[i] == '\\' { escapes += 1 continue @@ -201,6 +220,7 @@ func readString(expr string) (string, int, error) { if r == '"' && (escapes == 0 || escapes&1 == 0) { break } + // TODO: perhaps we should collapse the `\`, here? escapes = 0 } @@ -216,7 +236,7 @@ func readVariable(expr string) (string, int, error) { to := 1 // keep leading ':' for _, r := range expr[1:] { - to += 1 + to++ if r == ':' { break } @@ -232,6 +252,37 @@ func readVariable(expr string) (string, int, error) { return expr[:to], to, nil } +// the bool is an `ok` type bool, it is set to true if we successfull read a Bool +func readBool(expr string) (string, int, bool) { + to := 0 + +readString: + for _, r := range expr { + to++ + switch { + case r >= 'a' && r <= 'z', + r >= 'A' && r <= 'Z': + continue + case isBlankSpace(r): + // we read a potential bool + to-- // eject the space character we just read + break readString + default: + // not a bool + return "", 0, false + } + } + + switch expr[:to] { + case "True", "False": + // it's a Bool + return expr[:to], to, true + default: + // it isn't a Bool + return "", 0, false + } +} + var errFunctionNameWithoutParens = errors.New("function with Parenthesis") func readFunctionName(expr string) (string, int, error) { @@ -244,7 +295,7 @@ func readFunctionName(expr string) (string, int, error) { if isBlankSpace(r) { return expr[:to], to, errFunctionNameWithoutParens } - to += 1 + to++ } return "", 0, errors.Errorf("syntax error: missing '(' for function name '%s'", expr[:to]) @@ -267,7 +318,7 @@ func readFunctionArguments(expr string) (string, int, error) { continue } - to += 1 + to++ if r == '(' { bktCount++ continue @@ -322,7 +373,7 @@ func squashPlusMinusChain(expr string) (string, int) { if r == '-' { outcomeSign = -outcomeSign } - to += 1 + to++ } sign := "-" @@ -358,7 +409,6 @@ func readOperator(s string) (string, int) { strings.HasPrefix(s, Minus.String()), strings.HasPrefix(s, Divide.String()), strings.HasPrefix(s, Multiply.String()), - strings.HasPrefix(s, Power.String()), strings.HasPrefix(s, Modulus.String()), strings.HasPrefix(s, GreaterThan.String()), strings.HasPrefix(s, LessThan.String()): diff --git a/tree_builder_test.go b/tree_builder_test.go index bf4a996..07d0a10 100644 --- a/tree_builder_test.go +++ b/tree_builder_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/seborama/gal/v8" "github.com/stretchr/testify/require" + + "github.com/seborama/gal/v8" ) func TestTreeBuilder_FromExpr_VariousOperators(t *testing.T) { diff --git a/tree_test.go b/tree_test.go index 6dbeeff..929fc52 100644 --- a/tree_test.go +++ b/tree_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/seborama/gal/v8" "github.com/stretchr/testify/assert" + + "github.com/seborama/gal/v8" ) func TestTree_FullLen(t *testing.T) { diff --git a/value.go b/value.go index 70e4a6d..1a69b4f 100644 --- a/value.go +++ b/value.go @@ -10,14 +10,13 @@ import ( ) type Stringer interface { - String() string // TODO return String rather than string? Implications? + AsString() String // name is not String so to not clash with fmt.Stringer interface } type Numberer interface { Number() Number } -// TODO: any useful use of this??? type Booler interface { Bool() Bool } @@ -38,7 +37,7 @@ func ToNumber(val Value) Number { // TODO: ... MultiValue(...) - nothing stops the user from creating their own for now :-) // // TODO: implement other methods such as Add, LessThan, etc (if meaningful) -type MultiValue struct { // TODO: call this a ValueGroup? Or just a Group? +type MultiValue struct { Undefined values []Value } @@ -63,13 +62,16 @@ func (m MultiValue) Equal(other MultiValue) bool { return true } -// TODO: add test to confirm / illustrate output. func (m MultiValue) String() string { var vals []string for _, val := range m.values { vals = append(vals, val.String()) } - return `"` + strings.Join(vals, `","`) + `"` + return strings.Join(vals, `,`) +} + +func (m MultiValue) AsString() String { + return NewString(m.String()) } func (m MultiValue) Get(i int) Value { @@ -104,7 +106,7 @@ func (s String) Equal(other String) bool { func (s String) LessThan(other Value) Bool { if v, ok := other.(Stringer); ok { - return NewBool(s.value < v.String()) + return NewBool(s.value < v.AsString().value) } return False @@ -112,7 +114,7 @@ func (s String) LessThan(other Value) Bool { func (s String) LessThanOrEqual(other Value) Bool { if v, ok := other.(Stringer); ok { - return NewBool(s.value <= v.String()) + return NewBool(s.value <= v.AsString().value) } return False @@ -120,7 +122,7 @@ func (s String) LessThanOrEqual(other Value) Bool { func (s String) EqualTo(other Value) Bool { if v, ok := other.(Stringer); ok { - return NewBool(s.value == v.String()) + return NewBool(s.value == v.AsString().value) // beware to compare what's comparable: do NOT use s.value == v.String() because String() may decorate the value (see String and MultiValue for example) } return False @@ -132,7 +134,7 @@ func (s String) NotEqualTo(other Value) Bool { func (s String) GreaterThan(other Value) Bool { if v, ok := other.(Stringer); ok { - return NewBool(s.value > v.String()) + return NewBool(s.value > v.AsString().value) } return False @@ -140,7 +142,7 @@ func (s String) GreaterThan(other Value) Bool { func (s String) GreaterThanOrEqual(other Value) Bool { if v, ok := other.(Stringer); ok { - return NewBool(s.value >= v.String()) + return NewBool(s.value >= v.AsString().value) } return False @@ -148,7 +150,7 @@ func (s String) GreaterThanOrEqual(other Value) Bool { func (s String) Add(other Value) Value { if v, ok := other.(Stringer); ok { - return String{value: s.value + v.String()} + return String{value: s.value + v.AsString().value} } return NewUndefinedWithReasonf("cannot Add non-string to a string") @@ -174,11 +176,15 @@ func (s String) RShift(Value) Value { } func (s String) String() string { - return s.value + return `"` + s.value + `"` +} + +func (s String) AsString() String { + return s } func (s String) Number() Number { - n, err := NewNumberFromString(s.String()) + n, err := NewNumberFromString(s.value) // beware that `.String()` may decorate the value!! if err != nil { panic(err) // TODO :-/ } @@ -230,11 +236,6 @@ func (n Number) Add(other Value) Value { } func (n Number) Sub(other Value) Value { - switch v := other.(type) { - case Number: - return Number{value: n.value.Sub(v.value)} - } - if v, ok := other.(Numberer); ok { return Number{ value: n.value.Sub(v.Number().value), @@ -438,6 +439,10 @@ func (n Number) String() string { return n.value.String() } +func (n Number) AsString() String { + return NewString(n.String()) +} + func (n Number) Number() Number { return n } @@ -459,6 +464,18 @@ func NewBool(b bool) Bool { return Bool{value: b} } +// TODO: another option would be to return a Value and hence allow Undefined when neither True nor False is provided. +func NewBoolFromString(s string) (Bool, error) { + switch s { + case "True": + return True, nil + case "False": + return False, nil + default: + return False, errors.Errorf("'%s' cannot be converted to a Bool", s) + } +} + func (Bool) kind() entryKind { return valueEntryKind } @@ -467,9 +484,10 @@ func (Bool) kind() entryKind { func (b Bool) Equal(other Bool) bool { return b.value == other.value } + func (b Bool) EqualTo(other Value) Bool { - if v, ok := other.(Bool); ok { - return NewBool(b.value == v.value) + if v, ok := other.(Booler); ok { + return NewBool(b.value == v.Bool().value) } return False } @@ -500,15 +518,21 @@ func (b Bool) Bool() Bool { return b } -func (b Bool) String() string { // TODO: return String rather than string? +func (b Bool) String() string { if b.value { - return "true" + return "True" } - return "false" + return "False" } -var False = NewBool(false) -var True = NewBool(true) +func (b Bool) AsString() String { + return NewString(b.String()) +} + +var ( + False = NewBool(false) + True = NewBool(true) +) type Undefined struct { reason string // optional @@ -605,3 +629,7 @@ func (u Undefined) String() string { } return "undefined: " + u.reason } + +func (u Undefined) AsString() String { + return NewString(u.String()) +} diff --git a/value_test.go b/value_test.go new file mode 100644 index 0000000..ed2aee4 --- /dev/null +++ b/value_test.go @@ -0,0 +1,12 @@ +package gal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMultiValueString(t *testing.T) { + v := NewMultiValue(NewNumber(123), NewString("abc"), NewBool(true)) + assert.Equal(t, `123,"abc",True`, v.String()) +} diff --git a/variable.go b/variable.go index 6cf2d8f..8093463 100644 --- a/variable.go +++ b/variable.go @@ -15,5 +15,5 @@ func (Variable) kind() entryKind { } func (v Variable) String() string { - return string(v.Name) + return v.Name }