const std = @import("std");
const CalcError = error{
BadChar,
NotEnoughOperators,
TooLong,
Overflow,
TooManyOperators,
BadFloat,
};
const inputSize = 1024;
// rules of rpn dictate there must be 1 more operand than operators
// so we can never need a bigger stack than that.
const stackLength = (inputSize / 2) + 1;
fn calculateRpn(stream: []const u8) !f64 {
var stack: [inputSize:0]f64 = undefined;
var stackI: usize = 0;
var numberI: usize = 0;
var numberBuilder: [stackLength:0]u8 = undefined;
var i: usize = 0;
const streamLen = stream.len;
for (stream) |token| {
if (token == 0 or token == '\n') {
break;
}
switch (token) {
'0'...'9', '.' => {
numberBuilder[numberI] = token;
numberI += 1;
},
'_', ',' => {},
' ' => {
if (numberI == 0) {
continue;
}
const myFloat = std.fmt.parseFloat(f64, numberBuilder[0..numberI]) catch {
return CalcError.BadFloat;
// std.debug.print("bad float {s}, {any}", .{ numberBuilder[0..numberI], err });
// std.process.exit(1);
};
stack[stackI] = myFloat;
stackI += 1;
numberI = 0;
},
'-', '+', '*', '/' => {
if (token == '-') {
if (i < streamLen - 1 and (stream[i + 1] >= '0' and stream[i + 1] <= '9')) {
numberBuilder[numberI] = token;
numberI += 1;
continue;
}
}
if (numberI != 0) {
const myFloat = std.fmt.parseFloat(f64, numberBuilder[0..numberI]) catch {
return CalcError.BadFloat;
// std.debug.print("bad float {s}, {any}", .{ numberBuilder[0..numberI], err });
// std.process.exit(1);
};
stack[stackI] = myFloat;
stackI += 1;
numberI = 0;
}
if (stackI < 2) {
return CalcError.TooManyOperators;
// std.debug.print("not enough operands {c}", .{token});
// std.process.exit(1);
}
var result: f64 = undefined;
// std.debug.print("\n{d} {c} {d}:\n", .{ stack[stackI - 2], token, stack[stackI - 1] });
const left = stack[stackI - 2];
const right = stack[stackI - 1];
if (token == '+') {
result = left + right;
}
if (token == '-') {
result = left - right;
}
if (token == '*') {
result = left * right;
}
if (token == '/') {
result = left / right;
}
stack[stackI - 2] = result;
stackI -= 1;
},
else => {
return CalcError.BadChar;
},
}
i += 1;
}
if (stackI < 1) {
return CalcError.BadChar;
}
if (stackI > 1) {
return CalcError.NotEnoughOperators;
}
if (numberI != 0) {
return CalcError.NotEnoughOperators;
}
return stack[0];
}
const usage =
\\Enter the math you would like to perform, up to 1024 characters
\\valid characters: [0-9] - + * / . " "
\\This is a reverse polish notation calculator
\\e.g. 5 4 + 9 * 6 3 - /
\\NOT ((5+4)*9)/(6-3)
\\input:
\\
;
pub fn main() !void {
var args_it = std.process.args();
_ = args_it.next();
const inputVal = args_it.next();
var result: f64 = undefined;
if (inputVal != null) {
result = calculateRpn(inputVal.?) catch |err| {
std.debug.print("error processing input: {any}\n", .{err});
std.process.exit(1);
};
} else {
var input: [inputSize]u8 = undefined;
std.debug.print(usage, .{});
const stdin = std.io.getStdIn().reader();
_ = stdin.readUntilDelimiter(&input, '\n') catch |err| {
std.debug.print("error reading input: {any}\n", .{err});
std.process.exit(1);
};
result = calculateRpn(&input) catch |err| {
std.debug.print("error processing input: {any}\n", .{err});
std.process.exit(1);
};
}
const stdout_file = std.io.getStdOut().writer();
var bw = std.io.bufferedWriter(stdout_file);
const stdout = bw.writer();
defer {
bw.flush() catch |err| switch (err) {
else => {},
};
}
var bufA: [64]u8 = undefined;
var bufB: [64]u8 = undefined;
const twoDecimals = try std.fmt.formatFloat(&bufA, result, .{ .mode = .decimal, .precision = 3 });
const allDecimals = try std.fmt.formatFloat(&bufB, result, .{ .mode = .decimal });
if (twoDecimals.len < allDecimals.len) {
try stdout.print("{s}\n", .{twoDecimals});
} else {
try stdout.print("{s}\n", .{allDecimals});
}
}
test "happy paths" {
const expectEqual = std.testing.expectEqual;
const secondsInWeek = 60 * 60 * 24 * 7;
try expectEqual(6.0, try calculateRpn("3 3 +"));
try expectEqual(2.0, try calculateRpn("3 1.5 /"));
try expectEqual(3.142857142857143e0, try calculateRpn("22 7 /"));
try expectEqual(secondsInWeek, try calculateRpn("60 60 24 7 * * *"));
try expectEqual(secondsInWeek, try calculateRpn("60 60 * 24 * 7 *"));
}
test "supports commas" {
const expectEqual = std.testing.expectEqual;
try expectEqual(5044.1, calculateRpn("5,000 44.1 +"));
}
test "supports underscores" {
const expectEqual = std.testing.expectEqual;
try expectEqual(5044.1, calculateRpn("5_000 44.1 +"));
}
test "supports no spaces as long as not a minus" {
const expectEqual = std.testing.expectEqual;
try expectEqual(-1, calculateRpn("4 3 2 +-"));
try expectEqual(8, calculateRpn("2 2 2 **"));
}
test "invalid cases" {
const expectError = std.testing.expectError;
try expectError(CalcError.TooManyOperators, calculateRpn("60 60 * 24 * 7 * +"));
try expectError(CalcError.NotEnoughOperators, calculateRpn("60 60 * 24 * 7"));
try expectError(CalcError.BadChar, calculateRpn("oops text 3 3 +"));
}
test "non RPN not supported" {
const expectError = std.testing.expectError;
try expectError(CalcError.BadChar, calculateRpn("((5+4)*9)/(6-3)"));
try expectError(CalcError.TooManyOperators, calculateRpn("5+4"));
}
test "scientific notation not supported" {
const expectError = std.testing.expectError;
try expectError(CalcError.BadChar, calculateRpn("3e10 10 *"));
}