~andyc/oil

3f329477a20d4dc3a11ae1ab496b14a45a42b77e — Andy Chu 16 days ago 827f703
[oil-language] Implement ~== operator for Str, Int, Bool

Useful for CSV / TSV.

Related to #980.
3 files changed, 101 insertions(+), 22 deletions(-)

M doc/expression-language.md
M oil_lang/expr_eval.py
M spec/oil-expr.test.sh
M doc/expression-language.md => doc/expression-language.md +14 -5
@@ 130,27 130,36 @@ TODO:

### Exact Equality `=== !==`

- TODO: types must be the same, e.g. `'42' === 42` is not just false, it's an
  ERROR.

### Approximate Equality `~==`

- negation should use explicit `not`
Notes:

- There's no negative form like `!==`.  Instead, use `not (a ~== b)`.

Operands:

- LHS: Str
- RHS: Str, Int, Bool

    ' foo ' ~== 'foo'  # whitespace stripped on LEFT only
    ' 42 ' ~== 42
    ' 42.0 ' ~== 42.0
    ' true ' ~== true  # true, false, 0, 1, and I think T, F
    ' TRue ' ~== true  # true, false, 0, 1, and I think T, F

These also involve float conversion:
There's currently no float conversion.  These don't work:

    ' 42.0 ' ~== 42
    ' 42 ' ~== 42.0

I guess this means you need:
Or these:

    42.0 ~== 42
    42 ~== 42.0

I think `float_equals()` could be a separate function?

### Function and Method Calls

    var result = add(x, y)

M oil_lang/expr_eval.py => oil_lang/expr_eval.py +26 -1
@@ 396,7 396,32 @@ class OilEvaluator(object):

        elif op.id == Id.Expr_TildeDEqual:
          # Approximate equality
          e_die('~== not implemented')
          if not isinstance(left, str):
            e_die('~== expects a string on the left', span_id=op.span_id)

          left = left.strip()
          if isinstance(right, str):
            return left == right

          if isinstance(right, bool):  # Python quirk: must come BEFORE int
            left = left.lower()
            if left in ('true', '1'):
              left2 = True
            elif left in ('false', '0'):
              left2 = False
            else:
              return False

            log('left %r left2 %r', left, left2)
            return left2 == right

          if isinstance(right, int):
            if not left.isdigit():
              return False
            return int(left) == right

          e_die('~== expects Str, Int, or Bool on the right',
                span_id=op.span_id)

        else:
          try:

M spec/oil-expr.test.sh => spec/oil-expr.test.sh +61 -16
@@ 770,26 770,78 @@ ok
ok
## END

#### Approximate equality with ~==
#### Approximate equality of Str x {Str, Int, Bool} with ~==
shopt -s oil:all

# TODO: should we have !~== too?
# Or just make it not (a ~== b)
# Note: for now there's no !~== operator.  Use:   not (a ~== b)

if (3 ~== 3) {
  echo exact
if (' foo ' ~== 'foo') {
  echo Str-Str
}
if (' BAD ' ~== 'foo') {
  echo FAIL
}

if ('3 ' ~== 3) {
  echo Str-Int
}
if (1 ~== 3) {
if ('4 ' ~== '3') {
  echo FAIL
}

if (3 ~== '3') {
  echo int-str
if (' true ' ~== true) {
  echo Str-Bool
}
if (3 ~== '4') {
if (' true ' ~== false) {
  echo FAIL
}

const matrix = [
  ' TRue ' ~== true,  # case insentiive
  ' FALse ' ~== false,

  # Note this is the opposite of exit codes :-(
  # Maybe we should encourage 'if try' instead of 'if'
  ' 1 ' ~== true,
  ' 0 ' ~== false,
]

# = matrix
if (matrix === [true, true, true, true]) {
  echo 'bool matrix'
}

## STDOUT:
Str-Str
Str-Int
Str-Bool
bool matrix
## END

#### Wrong Types with ~==
shopt -s oil:all

# The LHS side should be a string

echo one
if (['1'] ~== ['1']) {
  echo bad
}
echo two

if (3 ~== 3) {
  echo bad
}

## status: 1
## STDOUT:
one
## END


#### Equality of ~== with Float (deferred)
shopt -s oil:all

if (42 ~== 42.0) {
  echo int-float
}


@@ 810,12 862,5 @@ if (42 ~== '42.0') {
if (42 ~== '43.0') {
  echo FAIL
}

## STDOUT:
exact
int-str
int-float
str-float
int-str-float
## END