Skip to content

Instantly share code, notes, and snippets.

@AndrewKraevskii
Last active October 18, 2024 14:14
Show Gist options
  • Save AndrewKraevskii/413f293630650f10df030edda4d55081 to your computer and use it in GitHub Desktop.
Save AndrewKraevskii/413f293630650f10df030edda4d55081 to your computer and use it in GitHub Desktop.
Simple calculator with precedence and parentheses
const std = @import("std");
const Presedence = u8;
const Operator = enum {
divide,
add,
subtract,
multiply,
pub fn fromChar(char: u8) ?Operator {
return switch (char) {
'+' => .add,
'-' => .subtract,
'*' => .multiply,
'/' => .divide,
else => null,
};
}
pub fn presedence(op: Operator) Presedence {
return switch (op) {
.add => 1,
.subtract => 1,
.multiply => 2,
.divide => 2,
};
}
pub fn execute(op: Operator, a: f64, b: f64) f64 {
return switch (op) {
.add => a + b,
.divide => a / b,
.subtract => a - b,
.multiply => a * b,
};
}
};
const Token = union(enum) {
number: f64,
operator: Operator,
@"(",
@")",
};
const base_ten_digits = "0123456789";
pub fn getToken(text: []const u8) error{ InvalidToken, Eof }!struct { Token, []const u8 } {
const trimmed = std.mem.trimLeft(u8, text, &std.ascii.whitespace);
if (trimmed.len == 0) return error.Eof;
switch (trimmed[0]) {
'0'...'9' => {
const token_len = std.mem.indexOfNone(u8, trimmed, base_ten_digits) orelse trimmed.len;
const num = std.fmt.parseFloat(f64, trimmed[0..token_len]) catch
return error.InvalidToken;
return .{ .{ .number = num }, trimmed[token_len..] };
},
'+', '-', '*', '/' => |op| {
return .{ .{ .operator = Operator.fromChar(op).? }, trimmed[1..] };
},
')', '(' => |paren| {
return .{
switch (paren) {
'(' => .@"(",
')' => .@")",
else => unreachable,
},
trimmed[1..],
};
},
else => {
return error.InvalidToken;
},
}
}
const TokenizeError = std.mem.Allocator.Error || error{InvalidToken};
pub fn tokenize(alloc: std.mem.Allocator, text: []const u8) TokenizeError![]const Token {
var tokens = std.ArrayListUnmanaged(Token){};
defer tokens.deinit(alloc);
var part_of_expression: []const u8 = text;
while (true) {
const token, part_of_expression = getToken(part_of_expression) catch |e| switch (e) {
error.Eof => return tokens.toOwnedSlice(alloc),
error.InvalidToken => return error.InvalidToken,
};
try tokens.append(alloc, token);
}
}
pub fn evaluate(expression: []const Token) !f64 {
if (expression.len == 0) return error.Empty;
var indent_level: u32 = 0;
for (expression) |token| {
if (token == .@"(") {
indent_level += 1;
} else if (token == .@")") {
indent_level = std.math.sub(u32, indent_level, 1) catch return error.ToManyClosingParenthesis;
}
}
if (indent_level != 0) return error.NotEnoughClosingParenthesis;
const res, const not_consumed_tokens = try evaluateInner(expression, 0);
std.debug.assert(not_consumed_tokens.len == 0);
return res;
}
fn evaluateInner(expression: []const Token, prev_presedence: Presedence) !struct { f64, []const Token } {
if (expression.len == 0) return error.Empty;
var tokens = expression;
var left = blk: switch (tokens[0]) {
.number => |n| {
tokens = tokens[1..];
break :blk n;
},
.operator => return error.EncounteredOperatorWhenNumberExpected,
.@"(" => {
const res, tokens = try evaluateInner(expression[1..], 0);
break :blk res;
},
.@")" => return error.@"Got ) when number expected",
};
while (true) {
if (tokens.len == 0) return .{ left, &.{} };
const op = switch (tokens[0]) {
.number => return error.GotNumberButExpectedOperator,
.operator => |op| op,
.@"(" => return error.@"Got ( when operator expected",
.@")" => return .{ left, tokens[1..] },
};
if (prev_presedence >= op.presedence()) {
return .{ left, tokens };
}
tokens = tokens[1..];
const right, tokens = try evaluateInner(tokens, op.presedence());
left = op.execute(left, right);
}
return .{ left, tokens };
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const tests = [_][]const u8{
"1",
"1 + 1",
"4 * 5 + 6",
"4 + 5 * 6",
"4 + 5 / 0",
"(1)",
"(1",
"1))",
")",
"(((2*((1+1+1+1)+10)))",
"(1 + 2) * 2",
"1 + (2 * 2)",
"4 + 5 / 0 + ",
"* 4 + 5 / 0 + ",
"4 - 5 - 10 * 2",
"(1 + 2) / 3",
"* ",
"",
};
var max_len: usize = 0;
for (tests) |@"test"| {
max_len = @max(max_len, @"test".len);
}
const out = std.io.getStdOut();
for (&tests) |@"test"| {
const tokens = try tokenize(alloc, @"test");
defer alloc.free(tokens);
const res = evaluate(tokens) catch |e| {
try out.writer().print("\"{s}\"", .{@"test"});
try out.writer().writeByteNTimes(' ', max_len - @"test".len);
try out.writer().print(" = {s}\n", .{@errorName(e)});
continue;
};
try out.writer().print("\"{s}\"", .{@"test"});
try out.writer().writeByteNTimes(' ', max_len - @"test".len);
try out.writer().print(" = {d}\n", .{res});
}
}
// Prints:
// "1" = 1
// "1 + 1" = 2
// "4 * 5 + 6" = 26
// "4 + 5 * 6" = 34
// "4 + 5 / 0" = inf
// "(1)" = 1
// "(1" = NotEnoughClosingParenthesis
// "1))" = ToManyClosingParenthesis
// ")" = ToManyClosingParenthesis
// "(((2*((1+1+1+1)+10)))" = NotEnoughClosingParenthesis
// "(1 + 2) * 2" = 6
// "1 + (2 * 2)" = 5
// "4 + 5 / 0 + " = Empty
// "* 4 + 5 / 0 + " = EncounteredOperatorWhenNumberExpected
// "4 - 5 - 10 * 2" = -21
// "(1 + 2) / 3" = 1
// "* " = EncounteredOperatorWhenNumberExpected
// "" = Empty
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment