====================
Expression Operators
====================

Tests for the MongoDB aggregation expression operators.

$Id$

Setup
-----

  >>> from m01.fake.expressions import resolve_expression
  >>> from m01.fake.expressions import get_field_value
  >>> from datetime import datetime


Helper: get_field_value
-----------------------

Get simple field:

  >>> doc = {'name': 'Alice', 'age': 30}
  >>> get_field_value(doc, 'name')
  'Alice'

Get nested field:

  >>> doc = {'user': {'name': 'Alice', 'address': {'city': 'Zurich'}}}
  >>> get_field_value(doc, 'user.name')
  'Alice'
  >>> get_field_value(doc, 'user.address.city')
  'Zurich'

Get from array:

  >>> doc = {'items': [{'name': 'a'}, {'name': 'b'}]}
  >>> get_field_value(doc, 'items.0.name')
  'a'
  >>> get_field_value(doc, 'items.1.name')
  'b'


Helper: resolve_expression
--------------------------

Field path:

  >>> doc = {'amount': 100, 'qty': 5}
  >>> resolve_expression('$amount', doc)
  100

Nested field path:

  >>> doc = {'order': {'amount': 200}}
  >>> resolve_expression('$order.amount', doc)
  200

Literal value:

  >>> resolve_expression(42, doc)
  42
  >>> resolve_expression('text', doc)
  'text'


Arithmetic Operators
--------------------

$add:

  >>> doc = {'a': 10, 'b': 5}
  >>> resolve_expression({'$add': ['$a', '$b']}, doc)
  15
  >>> resolve_expression({'$add': ['$a', 3, 2]}, doc)
  15

$subtract:

  >>> resolve_expression({'$subtract': ['$a', '$b']}, doc)
  5

$multiply:

  >>> resolve_expression({'$multiply': ['$a', '$b']}, doc)
  50

$divide:

  >>> resolve_expression({'$divide': ['$a', '$b']}, doc)
  2.0

$mod:

  >>> resolve_expression({'$mod': ['$a', 3]}, doc)
  1

$abs:

  >>> doc = {'val': -5}
  >>> resolve_expression({'$abs': '$val'}, doc)
  5

$ceil:

  >>> doc = {'val': 3.2}
  >>> resolve_expression({'$ceil': '$val'}, doc)
  4

$floor:

  >>> resolve_expression({'$floor': '$val'}, doc)
  3

$round:

  >>> doc = {'val': 3.567}
  >>> resolve_expression({'$round': ['$val', 2]}, doc)
  3.57
  >>> resolve_expression({'$round': ['$val', 0]}, doc)
  4.0

$sqrt:

  >>> doc = {'val': 16}
  >>> resolve_expression({'$sqrt': '$val'}, doc)
  4.0

$pow:

  >>> doc = {'base': 2, 'exp': 3}
  >>> resolve_expression({'$pow': ['$base', '$exp']}, doc)
  8.0


Comparison Operators
--------------------

$eq:

  >>> doc = {'a': 5, 'b': 5, 'c': 10}
  >>> resolve_expression({'$eq': ['$a', '$b']}, doc)
  True
  >>> resolve_expression({'$eq': ['$a', '$c']}, doc)
  False

$ne:

  >>> resolve_expression({'$ne': ['$a', '$c']}, doc)
  True

$gt:

  >>> resolve_expression({'$gt': ['$c', '$a']}, doc)
  True
  >>> resolve_expression({'$gt': ['$a', '$c']}, doc)
  False

$gte:

  >>> resolve_expression({'$gte': ['$a', '$b']}, doc)
  True
  >>> resolve_expression({'$gte': ['$c', '$a']}, doc)
  True

$lt:

  >>> resolve_expression({'$lt': ['$a', '$c']}, doc)
  True

$lte:

  >>> resolve_expression({'$lte': ['$a', '$b']}, doc)
  True

$cmp:

  >>> resolve_expression({'$cmp': ['$a', '$b']}, doc)
  0
  >>> resolve_expression({'$cmp': ['$a', '$c']}, doc)
  -1
  >>> resolve_expression({'$cmp': ['$c', '$a']}, doc)
  1


Boolean Operators
-----------------

$and:

  >>> doc = {}
  >>> resolve_expression({'$and': [True, True]}, doc)
  True
  >>> resolve_expression({'$and': [True, False]}, doc)
  False

$or:

  >>> resolve_expression({'$or': [True, False]}, doc)
  True
  >>> resolve_expression({'$or': [False, False]}, doc)
  False

$not:

  >>> resolve_expression({'$not': [True]}, doc)
  False
  >>> resolve_expression({'$not': [False]}, doc)
  True


Conditional Operators
---------------------

$cond (array form):

  >>> doc = {'score': 75}
  >>> resolve_expression({
  ...     '$cond': [{'$gte': ['$score', 70]}, 'pass', 'fail']
  ... }, doc)
  'pass'

$cond (object form):

  >>> resolve_expression({
  ...     '$cond': {
  ...         'if': {'$lt': ['$score', 70]},
  ...         'then': 'fail',
  ...         'else': 'pass'
  ...     }
  ... }, doc)
  'pass'

$ifNull:

  >>> doc = {'a': None, 'b': 'value'}
  >>> resolve_expression({'$ifNull': ['$a', 'default']}, doc)
  'default'
  >>> resolve_expression({'$ifNull': ['$b', 'default']}, doc)
  'value'
  >>> resolve_expression({'$ifNull': ['$missing', 'default']}, doc)
  'default'

$switch:

  >>> doc = {'status': 'active'}
  >>> resolve_expression({
  ...     '$switch': {
  ...         'branches': [
  ...             {'case': {'$eq': ['$status', 'active']}, 'then': 1},
  ...             {'case': {'$eq': ['$status', 'inactive']}, 'then': 0}
  ...         ],
  ...         'default': -1
  ...     }
  ... }, doc)
  1


Type Operators
--------------

$type:

  >>> doc = {'str': 'text', 'num': 42, 'arr': [1, 2], 'obj': {'a': 1}}
  >>> resolve_expression({'$type': '$str'}, doc)
  'string'
  >>> resolve_expression({'$type': '$num'}, doc)
  'int'
  >>> resolve_expression({'$type': '$arr'}, doc)
  'array'
  >>> resolve_expression({'$type': '$obj'}, doc)
  'object'

$toBool:

  >>> resolve_expression({'$toBool': 1}, doc)
  True
  >>> resolve_expression({'$toBool': 0}, doc)
  False

$toInt:

  >>> resolve_expression({'$toInt': 3.7}, doc)
  3

$toDouble:

  >>> resolve_expression({'$toDouble': 5}, doc)
  5.0

$toString:

  >>> resolve_expression({'$toString': 42}, doc)
  '42'


Date Operators
--------------

  >>> doc = {'date': datetime(2024, 6, 15, 10, 30, 45)}

$year:

  >>> resolve_expression({'$year': '$date'}, doc)
  2024

$month:

  >>> resolve_expression({'$month': '$date'}, doc)
  6

$dayOfMonth:

  >>> resolve_expression({'$dayOfMonth': '$date'}, doc)
  15

$hour:

  >>> resolve_expression({'$hour': '$date'}, doc)
  10

$minute:

  >>> resolve_expression({'$minute': '$date'}, doc)
  30

$second:

  >>> resolve_expression({'$second': '$date'}, doc)
  45

$dayOfWeek (1=Sunday, 7=Saturday):

  >>> resolve_expression({'$dayOfWeek': '$date'}, doc)
  7

$isoDayOfWeek (1=Monday, 7=Sunday):

  >>> resolve_expression({'$isoDayOfWeek': '$date'}, doc)
  6

$dateToString:

  >>> resolve_expression({
  ...     '$dateToString': {'date': '$date', 'format': '%Y-%m-%d'}
  ... }, doc)
  '2024-06-15'


String Operators
----------------

$concat:

  >>> doc = {'first': 'Hello', 'last': 'World'}
  >>> resolve_expression({'$concat': ['$first', ' ', '$last']}, doc)
  'Hello World'

$toLower:

  >>> resolve_expression({'$toLower': '$first'}, doc)
  'hello'

$toUpper:

  >>> resolve_expression({'$toUpper': '$first'}, doc)
  'HELLO'

$substr:

  >>> resolve_expression({'$substr': ['$first', 0, 3]}, doc)
  'Hel'

$strLenCP:

  >>> resolve_expression({'$strLenCP': '$first'}, doc)
  5

$split:

  >>> doc = {'text': 'a,b,c'}
  >>> resolve_expression({'$split': ['$text', ',']}, doc)
  ['a', 'b', 'c']

$trim:

  >>> doc = {'text': '  hello  '}
  >>> resolve_expression({'$trim': {'input': '$text'}}, doc)
  'hello'

$ltrim:

  >>> resolve_expression({'$ltrim': {'input': '$text'}}, doc)
  'hello  '

$rtrim:

  >>> resolve_expression({'$rtrim': {'input': '$text'}}, doc)
  '  hello'

$regexMatch:

  >>> doc = {'email': 'test@example.com'}
  >>> resolve_expression({
  ...     '$regexMatch': {'input': '$email', 'regex': r'@.*\.com$'}
  ... }, doc)
  True

$replaceOne:

  >>> doc = {'text': 'hello world'}
  >>> resolve_expression({
  ...     '$replaceOne': {'input': '$text', 'find': 'world', 'replacement': 'there'}
  ... }, doc)
  'hello there'


Array Operators
---------------

$size:

  >>> doc = {'items': [1, 2, 3, 4, 5]}
  >>> resolve_expression({'$size': '$items'}, doc)
  5

$arrayElemAt:

  >>> resolve_expression({'$arrayElemAt': ['$items', 0]}, doc)
  1
  >>> resolve_expression({'$arrayElemAt': ['$items', -1]}, doc)
  5

$first:

  >>> resolve_expression({'$first': '$items'}, doc)
  1

$last:

  >>> resolve_expression({'$last': '$items'}, doc)
  5

$slice:

  >>> resolve_expression({'$slice': ['$items', 2]}, doc)
  [1, 2]
  >>> resolve_expression({'$slice': ['$items', 1, 3]}, doc)
  [2, 3, 4]

$concatArrays:

  >>> doc = {'a': [1, 2], 'b': [3, 4]}
  >>> resolve_expression({'$concatArrays': ['$a', '$b']}, doc)
  [1, 2, 3, 4]

$in:

  >>> doc = {'items': ['apple', 'banana', 'cherry']}
  >>> resolve_expression({'$in': ['apple', '$items']}, doc)
  True
  >>> resolve_expression({'$in': ['grape', '$items']}, doc)
  False

$isArray:

  >>> resolve_expression({'$isArray': '$items'}, doc)
  True
  >>> doc = {'val': 'not array'}
  >>> resolve_expression({'$isArray': '$val'}, doc)
  False

$reverseArray:

  >>> doc = {'items': [1, 2, 3]}
  >>> resolve_expression({'$reverseArray': '$items'}, doc)
  [3, 2, 1]

$range:

  >>> resolve_expression({'$range': [0, 5]}, {})
  [0, 1, 2, 3, 4]
  >>> resolve_expression({'$range': [0, 10, 2]}, {})
  [0, 2, 4, 6, 8]

$filter:

  >>> doc = {'scores': [85, 92, 78, 95, 88]}
  >>> resolve_expression({
  ...     '$filter': {
  ...         'input': '$scores',
  ...         'as': 'score',
  ...         'cond': {'$gte': ['$$score', 90]}
  ...     }
  ... }, doc)
  [92, 95]

$map:

  >>> doc = {'nums': [1, 2, 3]}
  >>> resolve_expression({
  ...     '$map': {
  ...         'input': '$nums',
  ...         'as': 'n',
  ...         'in': {'$multiply': ['$$n', 2]}
  ...     }
  ... }, doc)
  [2, 4, 6]

$reduce:

  >>> doc = {'nums': [1, 2, 3, 4, 5]}
  >>> resolve_expression({
  ...     '$reduce': {
  ...         'input': '$nums',
  ...         'initialValue': 0,
  ...         'in': {'$add': ['$$value', '$$this']}
  ...     }
  ... }, doc)
  15


Object Operators
----------------

$mergeObjects:

  >>> doc = {'a': {'x': 1}, 'b': {'y': 2}}
  >>> result = resolve_expression({'$mergeObjects': ['$a', '$b']}, doc)
  >>> sorted(result.items())
  [('x', 1), ('y', 2)]

$objectToArray:

  >>> doc = {'obj': {'a': 1, 'b': 2}}
  >>> result = resolve_expression({'$objectToArray': '$obj'}, doc)
  >>> sorted([(d['k'], d['v']) for d in result])
  [('a', 1), ('b', 2)]

$arrayToObject:

  >>> doc = {'arr': [{'k': 'a', 'v': 1}, {'k': 'b', 'v': 2}]}
  >>> result = resolve_expression({'$arrayToObject': '$arr'}, doc)
  >>> sorted(result.items())
  [('a', 1), ('b', 2)]


$literal
--------

Return value as-is without parsing:

  >>> doc = {}
  >>> resolve_expression({'$literal': '$field'}, doc)
  '$field'
  >>> resolve_expression({'$literal': {'$add': [1, 2]}}, doc)
  {'$add': [1, 2]}


System Variables
----------------

$$ROOT:

  >>> doc = {'a': 1, 'b': 2}
  >>> result = resolve_expression('$$ROOT', doc)
  >>> sorted(result.items())
  [('a', 1), ('b', 2)]

$$CURRENT:

  >>> result = resolve_expression('$$CURRENT.a', doc)
  >>> result
  1
