Skip to content

Commit 1c75609

Browse files
committed
mongokit: support $mod query operator
1 parent 389d6e1 commit 1c75609

2 files changed

Lines changed: 183 additions & 0 deletions

File tree

mongokit/match.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func init() {
5050
ExpressionQueryOperators["$bitsAllSet"] = matchBits
5151
ExpressionQueryOperators["$bitsAnyClear"] = matchBits
5252
ExpressionQueryOperators["$bitsAnySet"] = matchBits
53+
ExpressionQueryOperators["$mod"] = matchMod
5354
}
5455

5556
// Match will test if the specified document matches the supplied MongoDB query
@@ -461,6 +462,89 @@ func matchElem(ctx Context, doc bsonkit.Doc, name, path string, v interface{}) e
461462
return ErrNotMatched
462463
}
463464

465+
func matchMod(_ Context, doc bsonkit.Doc, name, path string, v interface{}) error {
466+
// get array
467+
array, ok := v.(bson.A)
468+
if !ok {
469+
return fmt.Errorf("%s: expected array", name)
470+
}
471+
472+
// MongoDB requires exactly two elements: [divisor, remainder]
473+
if len(array) != 2 {
474+
return fmt.Errorf("%s: expected array of two elements", name)
475+
}
476+
477+
// parse divisor and remainder; doubles are truncated toward zero
478+
divisor, err := modOperandToInt64(name, "divisor", array[0])
479+
if err != nil {
480+
return err
481+
}
482+
remainder, err := modOperandToInt64(name, "remainder", array[1])
483+
if err != nil {
484+
return err
485+
}
486+
487+
// reject zero divisor
488+
if divisor == 0 {
489+
return fmt.Errorf("%s: divisor cannot be zero", name)
490+
}
491+
492+
return matchUnwind(doc, path, true, false, func(field interface{}) error {
493+
// non-numeric or non-finite fields do not match
494+
n, ok := numberToInt64(field)
495+
if !ok {
496+
return ErrNotMatched
497+
}
498+
if n%divisor != remainder {
499+
return ErrNotMatched
500+
}
501+
return nil
502+
})
503+
}
504+
505+
func modOperandToInt64(name, role string, v interface{}) (int64, error) {
506+
switch n := v.(type) {
507+
case int32:
508+
return int64(n), nil
509+
case int64:
510+
return n, nil
511+
case float64:
512+
if math.IsNaN(n) {
513+
return 0, fmt.Errorf("%s: %s cannot be NaN", name, role)
514+
}
515+
if math.IsInf(n, 0) {
516+
return 0, fmt.Errorf("%s: %s cannot be infinity", name, role)
517+
}
518+
// reject doubles that fall outside the int64 range; -float64(MinInt64)
519+
// is exactly 2^63, the smallest float strictly above MaxInt64
520+
if n < float64(math.MinInt64) || n >= -float64(math.MinInt64) {
521+
return 0, fmt.Errorf("%s: %s out of range", name, role)
522+
}
523+
return int64(math.Trunc(n)), nil
524+
default:
525+
return 0, fmt.Errorf("%s: %s must be a number", name, role)
526+
}
527+
}
528+
529+
func numberToInt64(v interface{}) (int64, bool) {
530+
switch n := v.(type) {
531+
case int32:
532+
return int64(n), true
533+
case int64:
534+
return n, true
535+
case float64:
536+
if math.IsNaN(n) || math.IsInf(n, 0) {
537+
return 0, false
538+
}
539+
if n < float64(math.MinInt64) || n >= -float64(math.MinInt64) {
540+
return 0, false
541+
}
542+
return int64(math.Trunc(n)), true
543+
default:
544+
return 0, false
545+
}
546+
}
547+
464548
func matchBits(_ Context, doc bsonkit.Doc, op, path string, v interface{}) error {
465549
// parse the bitmask once into a list of bit positions
466550
positions, err := parseBitMask(op, v)

mongokit/match_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,3 +1447,102 @@ func TestMatchBits(t *testing.T) {
14471447
}, false)
14481448
})
14491449
}
1450+
1451+
func TestMatchMod(t *testing.T) {
1452+
matchTest(t, bson.M{
1453+
"foo": int32(10),
1454+
}, func(fn func(bson.M, interface{})) {
1455+
// invalid argument shapes
1456+
fn(bson.M{
1457+
"foo": bson.M{"$mod": int32(1)},
1458+
}, "$mod: expected array")
1459+
fn(bson.M{
1460+
"foo": bson.M{"$mod": bson.A{int32(3)}},
1461+
}, "$mod: expected array of two elements")
1462+
fn(bson.M{
1463+
"foo": bson.M{"$mod": bson.A{int32(1), int32(2), int32(3)}},
1464+
}, "$mod: expected array of two elements")
1465+
fn(bson.M{
1466+
"foo": bson.M{"$mod": bson.A{"x", int32(0)}},
1467+
}, "$mod: divisor must be a number")
1468+
fn(bson.M{
1469+
"foo": bson.M{"$mod": bson.A{int32(2), "x"}},
1470+
}, "$mod: remainder must be a number")
1471+
fn(bson.M{
1472+
"foo": bson.M{"$mod": bson.A{int32(0), int32(0)}},
1473+
}, "$mod: divisor cannot be zero")
1474+
1475+
// matching int32 field
1476+
fn(bson.M{
1477+
"foo": bson.M{"$mod": bson.A{int32(3), int32(1)}},
1478+
}, true)
1479+
fn(bson.M{
1480+
"foo": bson.M{"$mod": bson.A{int32(3), int32(2)}},
1481+
}, false)
1482+
1483+
// int64 / float64 operands are accepted and truncated
1484+
fn(bson.M{
1485+
"foo": bson.M{"$mod": bson.A{int64(4), int64(2)}},
1486+
}, true)
1487+
fn(bson.M{
1488+
"foo": bson.M{"$mod": bson.A{3.7, 1.9}},
1489+
}, true) // truncated to (3, 1) → 10 % 3 == 1
1490+
fn(bson.M{
1491+
"foo": bson.M{"$mod": bson.A{3.0, 1.0}},
1492+
}, true)
1493+
})
1494+
1495+
// fractional float field is truncated toward zero
1496+
matchTest(t, bson.M{
1497+
"foo": 10.7,
1498+
}, func(fn func(bson.M, interface{})) {
1499+
fn(bson.M{
1500+
"foo": bson.M{"$mod": bson.A{int32(3), int32(1)}},
1501+
}, true) // trunc(10.7)=10, 10%3=1
1502+
fn(bson.M{
1503+
"foo": bson.M{"$mod": bson.A{int32(3), int32(2)}},
1504+
}, false)
1505+
})
1506+
1507+
// negative dividend retains sign of dividend in remainder
1508+
matchTest(t, bson.M{
1509+
"foo": int32(-7),
1510+
}, func(fn func(bson.M, interface{})) {
1511+
fn(bson.M{
1512+
"foo": bson.M{"$mod": bson.A{int32(3), int32(-1)}},
1513+
}, true)
1514+
fn(bson.M{
1515+
"foo": bson.M{"$mod": bson.A{int32(3), int32(2)}},
1516+
}, false)
1517+
})
1518+
1519+
// non-numeric field never matches
1520+
matchTest(t, bson.M{
1521+
"foo": "bar",
1522+
}, func(fn func(bson.M, interface{})) {
1523+
fn(bson.M{
1524+
"foo": bson.M{"$mod": bson.A{int32(1), int32(0)}},
1525+
}, false)
1526+
})
1527+
1528+
// missing field never matches
1529+
matchTest(t, bson.M{
1530+
"foo": int32(1),
1531+
}, func(fn func(bson.M, interface{})) {
1532+
fn(bson.M{
1533+
"bar": bson.M{"$mod": bson.A{int32(1), int32(0)}},
1534+
}, false)
1535+
})
1536+
1537+
// array field unwinds: matches if any element matches
1538+
matchTest(t, bson.M{
1539+
"foo": bson.A{int32(3), int32(7), int32(10)},
1540+
}, func(fn func(bson.M, interface{})) {
1541+
fn(bson.M{
1542+
"foo": bson.M{"$mod": bson.A{int32(5), int32(0)}},
1543+
}, true) // 10 % 5 == 0
1544+
fn(bson.M{
1545+
"foo": bson.M{"$mod": bson.A{int32(5), int32(4)}},
1546+
}, false)
1547+
})
1548+
}

0 commit comments

Comments
 (0)