Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Calculator conundrum concept exercise #307

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions concepts/error-handling/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ There is also a special macro, called `panic!`, that allows a `ByteArray` to be
panic!("The error for the panic! Error message is not limited to 31 characters anymore");
```

### The `assert!` Macro

The `assert!` macro is a useful tool for enforcing specific conditions in your code.
If the condition in `assert!` evaluates to `false`, the program will panic with a `ByteArray` error message.
This is often used to verify assumptions during development and ensure values meet certain criteria.

For example:

```rust
fn main() {
let x = 5;
assert!(x > 0, "x must be greater than zero");
}
```

If `x` is not greater than zero, the program will panic with the message `"x must be greater than zero"`. `assert!` is helpful for checking invariants and preconditions without manually writing error-handling code.

### `nopanic` Notation

Cairo `nopanic` notation indicates that a function will never panic.
Expand Down
4 changes: 4 additions & 0 deletions concepts/error-handling/links.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
{
"url": "https://book.cairo-lang.org/ch09-00-error-handling.html",
"description": "Error handling in the Cairo book"
},
{
"url": "https://book.cairo-lang.org/ch11-05-macros.html?highlight=assert#assert-and-assert_xx-macros",
"description": "assert! macro"
}
]
9 changes: 5 additions & 4 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,15 @@
"status": "wip"
},
{
"slug": "the-farm",
"name": "The Farm",
"uuid": "e167e30c-84b1-44da-b21c-70bc46688f20",
"slug": "calculator-conundrum",
"name": "Calculator Conundrum",
"uuid": "989df4b1-6d89-468d-96e0-47013e3cf99b",
"concepts": [
"error-handling"
],
"prerequisites": [
"structs"
"traits",
"option"
],
"status": "wip"
},
Expand Down
25 changes: 25 additions & 0 deletions exercises/concept/calculator-conundrum/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Hints

## General

- [Unrecoverable Errors][unrecoverable]: invoke a panic with error message.
- [`panic!` Macro][panic-excl-macro]: invoke a panic with `ByteArray` error message.
- [`assert!` Macro][assert]: invoke a panic if a condition evaluates to `false`.
- [Result Enum][result]: how to use the `Result` enum.

## 2. Handle illegal operations

- You need to [return an error][result] here.

## 3. Handle no operation provided

- You need to [panic][panic-excl-macro] here with a `ByteArray` message to handle empty strings for operations.

## 4. Handle errors when dividing by zero

- You need to panic here to handle division by zero.

[unrecoverable]: https://book.cairo-lang.org/ch09-01-unrecoverable-errors-with-panic.html#unrecoverable-errors-with-panic
[panic-excl-macro]: https://book.cairo-lang.org/ch09-01-unrecoverable-errors-with-panic.html#panic-macro
[assert]: https://book.cairo-lang.org/ch11-05-macros.html?highlight=assert#assert-and-assert_xx-macros
[result]: https://book.cairo-lang.org/ch09-02-recoverable-errors.html#the-result-enum
46 changes: 46 additions & 0 deletions exercises/concept/calculator-conundrum/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Instructions

In this exercise, you will be building error handling for a simple integer calculator.
The calculator should support addition, multiplication, and division operations, returning the result as a formatted string.
You will also implement error handling to address illegal operations and division by zero.

The goal is to have a working calculator that returns a string in the following format: `16 + 51 = 67`, when provided with arguments `16`, `51`, and `+`.

```rust
SimpleCalculator::calculate(16, 51, "+"); // => returns "16 + 51 = 67"

SimpleCalculator::calculate(32, 6, "*"); // => returns "32 * 6 = 192"

SimpleCalculator::calculate(512, 4, "/"); // => returns "512 / 4 = 128"
```

## 1. Implement the calculator operations

The main function for this task will be `SimpleCalculator::calculate`, which takes three arguments: two integers and a `ByteArray` representing the operation.
Implement the following operations:

- **Addition** with the `+` symbol
- **Multiplication** with the `*` symbol
- **Division** with the `/` symbol

## 2. Handle illegal operations

If the operation symbol is anything other than `+`, `*`, or `/`, the calculator should either panic or return an error:

- If the operation is an empty string, panic with a `ByteArray` error message `"Operation cannot be an empty string"`.
- For any other invalid operation, return `Result::Err("Operation is out of range")`.

```rust
SimpleCalculator::calculate(100, 10, "-"); // => returns Result::Err("Operation is out of range")

SimpleCalculator::calculate(8, 2, ""); // => panics with "Operation cannot be an empty string"
```

## 3. Handle errors when dividing by zero

When attempting to divide by `0`, the calculator should panic with an error message indicating that division by zero is not allowed.
The returned result should be a `felt252` value of `'Division by zero is not allowed'`.

```rust
SimpleCalculator::calculate(512, 0, "/"); // => panics with 'Division by zero is not allowed'
```
65 changes: 65 additions & 0 deletions exercises/concept/calculator-conundrum/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Introduction

In programming, it's essential to handle errors gracefully to ensure that unexpected situations do not cause a program to crash or behave unpredictably.
Cairo provides two main mechanisms for error handling:

1. **Unrecoverable errors** with `panic`, which immediately stop the program.
2. **Recoverable errors** with `Result`, which allow the program to handle and respond to errors.

## Unrecoverable Errors with `panic`

Sometimes, a program encounters an error so severe that it cannot proceed.
Cairo uses the `panic` function to immediately stop execution in such cases.
This is helpful when an error, like attempting to access an out-of-bounds index, makes it impossible for the program to continue in a sensible way.
The `panic` function accepts a `ByteArray` message that describes the reason for the error.

For example, in Cairo:

```rust
fn main() {
let data = array![1, 2];
panic("An unrecoverable error has occurred!");
}
```

This example demonstrates a forced panic, which immediately stops the program with a message.

### The `assert!` Macro

The `assert!` macro is a useful tool for enforcing specific conditions in your code.
If the condition in `assert!` evaluates to `false`, the program will panic with a `ByteArray` error message.
This is often used to verify assumptions during development and ensure values meet certain criteria.

For example:

```rust
fn main() {
let x = 5;
assert!(x > 0, "x must be greater than zero");
}
```

If `x` is not greater than zero, the program will panic with the message `"x must be greater than zero"`. `assert!` is helpful for checking invariants and preconditions without manually writing error-handling code.

## Recoverable Errors with `Result`

Not all errors need to stop the program.
Some can be handled gracefully so the program can continue.
Cairo's `Result` enum represents these recoverable errors, and it has two variants:

- `Result::Ok` indicates success.
- `Result::Err` represents an error.

Using `Result`, a function can return either a success value or an error, allowing the calling function to decide what to do next.

```rust
fn divide(a: u32, b: u32) -> Result<u32, ByteArray> {
if b == 0 {
Result::Err("Error: Division by zero")
} else {
Result::Ok(a / b)
}
}
```

In this example, if `b` is zero, an error is returned; otherwise, the result of the division is returned.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"authors": [
"<your_gh_username>"
"0xNeshi"
],
"files": {
"solution": [
"src/lib.cairo"
],
"test": [
"tests/the_farm.cairo"
"tests/calculator_conundrum.cairo"
],
"exemplar": [
".meta/exemplar.cairo"
Expand All @@ -17,7 +17,7 @@
]
},
"forked_from": [
"go/the-farm"
"csharp/calculator-conundrum"
],
"blurb": "<blurb>"
"blurb": "Learn about error handling by working on a simple calculator."
}
27 changes: 27 additions & 0 deletions exercises/concept/calculator-conundrum/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Design

## Goal

Introduce the student to error handling in Cairo.

## Learning objectives

- know how create recoverable errors.
- know how create unrecoverable errors.

## Out of scope

## Concepts

- error-handling

## Prerequisites

- traits
- option

## Resources to refer to

- [Cairo Book - Error Handling][error-handling]

[error-handling]: https://book.cairo-lang.org/ch09-00-error-handling.html
17 changes: 17 additions & 0 deletions exercises/concept/calculator-conundrum/.meta/exemplar.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#[generate_trait]
pub impl SimpleCalculatorImpl of SimpleCalculatorTrait {
fn calculate(a: i32, b: i32, operation: ByteArray) -> Result<ByteArray, ByteArray> {
assert!(operation != "", "Operation cannot be an empty string");

if operation == "+" {
Result::Ok(format!("{} + {} = {}", a, b, a + b))
} else if operation == "*" {
Result::Ok(format!("{} * {} = {}", a, b, a * b))
} else if operation == "/" {
assert(b != 0, 'Division by zero is not allowed');
Result::Ok(format!("{} / {} = {}", a, b, a / b))
} else {
Result::Err("Operation is out of range")
}
}
}
7 changes: 7 additions & 0 deletions exercises/concept/calculator-conundrum/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "calculator_conundrum"
version = "0.1.0"
edition = "2024_07"

[dev-dependencies]
cairo_test = "2.8.2"
6 changes: 6 additions & 0 deletions exercises/concept/calculator-conundrum/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[generate_trait]
pub impl SimpleCalculatorImpl of SimpleCalculatorTrait {
fn calculate(a: i32, b: i32, operation: ByteArray) -> Result<ByteArray, ByteArray> {
panic!("implement `calculate`")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use calculator_conundrum::SimpleCalculatorTrait as SimpleCalculator;

#[test]
fn addition_with_small_operands() {
assert_eq!(SimpleCalculator::calculate(22, 25, "+").unwrap(), "22 + 25 = 47");
}

#[test]
#[ignore]
fn addition_with_large_operands() {
assert_eq!(
SimpleCalculator::calculate(378_961, 399_635, "+").unwrap(), "378961 + 399635 = 778596"
);
}

#[test]
#[ignore]
fn multiplication_with_small_operands() {
assert_eq!(SimpleCalculator::calculate(3, 21, "*").unwrap(), "3 * 21 = 63");
}

#[test]
#[ignore]
fn multiplication_with_large_operands() {
assert_eq!(
SimpleCalculator::calculate(72_441, 2_048, "*").unwrap(), "72441 * 2048 = 148359168"
);
}

#[test]
#[ignore]
fn division_with_small_operands() {
assert_eq!(SimpleCalculator::calculate(72, 9, "/").unwrap(), "72 / 9 = 8");
}

#[test]
#[ignore]
fn division_with_large_operands() {
assert_eq!(
SimpleCalculator::calculate(1_338_800, 83_675, "/").unwrap(), "1338800 / 83675 = 16"
);
}

#[test]
#[ignore]
fn calculate_returns_result_err_for_non_valid_operations() {
assert_eq!(SimpleCalculator::calculate(1, 2, "**").unwrap_err(), "Operation is out of range");
}

#[test]
#[ignore]
#[should_panic(expected: ("Operation cannot be an empty string",))]
fn calculate_returns_result_err_for_empty_string_as_operation() {
let _ = SimpleCalculator::calculate(1, 2, "");
}

#[test]
#[ignore]
#[should_panic(expected: ('Division by zero is not allowed',))]
fn calculate_panics_for_division_with_0() {
let _ = SimpleCalculator::calculate(33, 0, "/");
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions single-sentence-per-line-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ module.exports = {
"e.g",
"etc",
"ex",
"`assert",
"`panic",
];
const lineEndings = params.config.line_endings || [".", "?", "!"];
const sentenceStartRegex =
Expand Down
Loading