Skip to content

Commit

Permalink
feat: modulo operator (#1256)
Browse files Browse the repository at this point in the history
### Summary of Changes

Add the modulo operator `%`. It follows Python's floor-based
implementation and defines `n % d` as

$$
n - \left\lfloor\frac{n}{d}\right\rfloor \cdot d
$$
  • Loading branch information
lars-reimann authored Nov 2, 2024
1 parent c8f3b67 commit f590e7a
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 25 deletions.
38 changes: 26 additions & 12 deletions docs/api/safeds/data/tabular/containers/Cell.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,21 @@ This class cannot be instantiated directly. It is only used for arguments of cal
) -> result: Cell<Number>

/**
* Perform a modulo operation.
* Perform a modulo operation. This is equivalent to the `%` operator.
*
* @example
* pipeline example {
* val column = Column("example", [5, 6]);
* val result = column.transform((cell) -> cell.mod(3));
* // Column("example", [2, 0])
* }
*
* @example
* pipeline example {
* val column = Column("example", [5, 6]);
* val result = column.transform((cell) -> cell % 3);
* // Column("example", [2, 0])
* }
*/
@Pure
fun mod(
Expand Down Expand Up @@ -661,7 +668,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="309"
```sds linenums="316"
@Pure
fun eq(
other: Any?
Expand Down Expand Up @@ -730,7 +737,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="353"
```sds linenums="360"
@Pure
fun ge(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down Expand Up @@ -772,7 +779,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="375"
```sds linenums="382"
@Pure
fun gt(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down Expand Up @@ -814,7 +821,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="397"
```sds linenums="404"
@Pure
fun le(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down Expand Up @@ -856,7 +863,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="419"
```sds linenums="426"
@Pure
fun lt(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand All @@ -865,7 +872,7 @@ pipeline example {

## <code class="doc-symbol doc-symbol-function"></code> `mod` {#safeds.data.tabular.containers.Cell.mod data-toc-label='[function] mod'}

Perform a modulo operation.
Perform a modulo operation. This is equivalent to the `%` operator.

**Parameters:**

Expand All @@ -888,10 +895,17 @@ pipeline example {
// Column("example", [2, 0])
}
```
```sds
pipeline example {
val column = Column("example", [5, 6]);
val result = column.transform((cell) -> cell % 3);
// Column("example", [2, 0])
}
```

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="228"
```sds linenums="235"
@Pure
fun mod(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down Expand Up @@ -933,7 +947,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="250"
```sds linenums="257"
@Pure
fun mul(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down Expand Up @@ -1009,7 +1023,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="331"
```sds linenums="338"
@Pure
fun neq(
other: Any?
Expand Down Expand Up @@ -1122,7 +1136,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="265"
```sds linenums="272"
@Pure
fun pow(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down Expand Up @@ -1164,7 +1178,7 @@ pipeline example {

??? quote "Stub code in `Cell.sdsstub`"

```sds linenums="287"
```sds linenums="294"
@Pure
fun ^sub(
other: union<Number, Cell> // TODO, once cell types can be inferred: union<Number, Cell<Number>>
Expand Down
28 changes: 19 additions & 9 deletions docs/pipeline-language/expressions/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ The usual arithmetic operations are also supported for integers, floats and comb
- Subtraction: `#!sds 6 - 2.9` (result is a float)
- Multiplication: `#!sds 1.1 * 3` (result is a float)
- Division: `#!sds 1.0 / 4.2` (result is a float)
- Modulo: `#!sds 5 % 2` (result is an integer)

The `%` operator is well-known from other programming languages. However, there is no consensus on its result for negative operands. Safe-DS defines `n % d` as $n - \left\lfloor\frac{n}{d}\right\rfloor \cdot d$, where $\left\lfloor\text{ }\right\rfloor$ is the floor function (rounding down). The result always has the same sign as the divisor `d`. The following table shows some examples:

| `n` | `d` | `n % d` |
|-----|-----|---------|
| 5 | 3 | 2 |
| 5 | -3 | -1 |
| -5 | 3 | 1 |
| -5 | -3 | -2 |

Finally, two numbers can be compared, which results in a boolean. The integer `#!sds 3` for example is less than the integer `#!sds 5`. Safe-DS offers operators to do such checks for order:

Expand All @@ -33,22 +43,22 @@ To work with logic, Safe-DS has the two boolean literals `#!sds false` and `#!sd
- (Logical) **negation** (example `#!sds not a`): Output is `#!sds true` if and only if the operand is false:

| `#!sds not a` | false | true |
|---------|-------|-------|
| &nbsp; | true | false |
|---------------|-------|-------|
| &nbsp; | true | false |

- **Conjunction** (example `#!sds a and b`): Output is `#!sds true` if and only if both operands are `#!sds true`. Note that the second operand is always evaluated, even if the first operand is `#!sds false` and, thus, already determines the result of the expression. The operator is not short-circuited:

| `#!sds a and b` | false | true |
|-----------|-------|-------|
| **false** | false | false |
| **true** | false | true |
|-----------------|-------|-------|
| **false** | false | false |
| **true** | false | true |

- **Disjunction** (example `#!sds a or b`): Output is `#!sds true` if and only if at least one operand is `#!sds true`. Note that the second operand is always evaluated, even if the first operand is `#!sds true` and, thus, already determines the result of the expression. The operator is not short-circuited:

| `#!sds a or b` | false | true |
|-----------|-------|------|
| **false** | false | true |
| **true** | true | true |
| `#!sds a or b` | false | true |
|----------------|-------|------|
| **false** | false | true |
| **true** | true | true |

## Equality Checks

Expand Down
2 changes: 1 addition & 1 deletion docs/pipeline-language/expressions/precedence.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ We all know that `#!sds 2 + 3 * 7` is `#!sds 23` and not `#!sds 35`. The reason
- `#!sds -` (unary, [arithmetic negations][operations-on-numbers])
- `#!sds as` ([type casts][type-casts])
- `#!sds ?:` ([Elvis operators][elvis-operator])
- `#!sds *`, `#!sds /` ([multiplicative operators][operations-on-numbers])
- `#!sds *`, `#!sds /`, `#!sds %` ([multiplicative operators][operations-on-numbers])
- `#!sds +`, `#!sds -` (binary, [additive operators][operations-on-numbers])
- `#!sds <`, `#!sds <=`, `#!sds >=`, `#!sds >` ([comparison operators][operations-on-numbers])
- `#!sds ===`, `#!sds ==`, `#!sds !==`, `#!sds !=` ([equality operators][equality-checks])
Expand Down
2 changes: 1 addition & 1 deletion packages/safe-ds-lang/src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ SdsMultiplicativeExpression returns SdsExpression:
;

SdsMultiplicativeOperator returns string:
'*' | '/'
'*' | '/' | '%'
;

SdsElvisExpression returns SdsExpression:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,18 @@ export class SafeDsPartialEvaluator {
(leftOperand, rightOperand) => leftOperand / rightOperand,
evaluatedRight,
);
case '%':
// Division by zero
if (zeroConstants.some((it) => it.equals(evaluatedRight))) {
return UnknownEvaluatedNode;
}

return this.evaluateArithmeticOp(
evaluatedLeft,
(leftOperand, rightOperand) => ((leftOperand % rightOperand) + rightOperand) % rightOperand,
(leftOperand, rightOperand) => ((leftOperand % rightOperand) + rightOperand) % rightOperand,
evaluatedRight,
);

/* c8 ignore next 2 */
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ export class SafeDsTypeComputer {
case '-':
case '*':
case '/':
case '%':
return this.computeTypeOfArithmeticInfixOperation(node);

// Elvis operator
Expand Down
1 change: 1 addition & 0 deletions packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export const infixOperationOperandsMustHaveCorrectType = (services: SafeDsServic
case '-':
case '*':
case '/':
case '%':
if (
node.leftOperand &&
!typeChecker.isSubtypeOf(leftType, coreTypes.Float) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,21 @@ class Cell<out T = Any?> {
) -> result: Cell<Number>

/**
* Perform a modulo operation.
* Perform a modulo operation. This is equivalent to the `%` operator.
*
* @example
* pipeline example {
* val column = Column("example", [5, 6]);
* val result = column.transform((cell) -> cell.mod(3));
* // Column("example", [2, 0])
* }
*
* @example
* pipeline example {
* val column = Column("example", [5, 6]);
* val result = column.transform((cell) -> cell % 3);
* // Column("example", [2, 0])
* }
*/
@Pure
fun mod(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pipeline myPipeline {
1 % 2;
}

// -----------------------------------------------------------------------------

pipeline myPipeline {
1 % 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ def test():
f((cell()) / (h()))
f((h()) / (cell()))
f((cell()) / (cell()))
f((h()) % (h()))
f((cell()) % (h()))
f((h()) % (cell()))
f((cell()) % (cell()))
f(__gen_eager_elvis(i(), i()))

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ pipeline test {
f(h() / cell());
f(cell() / cell());

f(h() % h());
f(cell() % h());
f(h() % cell());
f(cell() % cell());


f(i() ?: i());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ syntax_error

pipeline myPipeline {
% 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ syntax_error

pipeline myPipeline {
1 %;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ no_syntax_error

pipeline myPipeline {
1 % 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tests.partialValidation.recursiveCases.infixOperations.modulo

pipeline test {
// $TEST$ serialization 0.25
»0.25 % 0.5«;

// $TEST$ serialization 0.5
»1.5 % 1«;

// $TEST$ serialization 0.375
»1 % 0.625«;

// $TEST$ serialization 0
»1 % 1«;

// $TEST$ serialization -1
»3 % -2«;

// $TEST$ serialization 1
»-3 % 2«;

// $TEST$ serialization -1
»-3 % -2«;


// $TEST$ serialization ?
»1 % 0«;

// $TEST$ serialization ?
»1 % 0.0«;

// $TEST$ serialization ?
»1 % -0.0«;


// $TEST$ serialization ?
»true % 1«;

// $TEST$ serialization ?
»1 % true«;

// $TEST$ serialization ?
»unresolved % 1«;

// $TEST$ serialization ?
»1 % unresolved«;
}
Loading

0 comments on commit f590e7a

Please sign in to comment.