const std = @import("std"); const Value = @import("./value.zig").Value; const Array = @import("./value.zig").Array; const Object = @import("./value.zig").Object; pub const OpsError = error{ InvalidType, InternalError, Unimplemented, }; pub fn index(base: Value, key: Value) OpsError!Value { return switch (base) { .array => |a| switch (key) { .integer => |i| a.get(@intCast(i)), else => error.InvalidType, }, .object => |o| switch (key) { .string => |s| o.get(s) orelse Value.null, else => error.InvalidType, }, .null => Value.null, else => error.InvalidType, }; } pub fn slice(allocator: std.mem.Allocator, base: Value, from: Value, to: Value) OpsError!Value { switch (base) { .array => |a| { const len: i64 = @intCast(a.len()); var start: i64 = if (from == .null) 0 else if (from == .integer) from.integer else return error.InvalidType; var end: i64 = if (to == .null) len else if (to == .integer) to.integer else return error.InvalidType; if (start < 0) start += len; if (end < 0) end += len; start = @max(0, @min(start, len)); end = @max(0, @min(end, len)); if (start >= end) { const empty = Array.init(allocator) catch return error.InternalError; return Value.initArray(empty); } var result = Array.init(allocator) catch return error.InternalError; const ustart: usize = @intCast(start); const uend: usize = @intCast(end); var i: usize = ustart; while (i < uend) : (i += 1) { result.append(allocator, a.get(i)) catch return error.InternalError; } return Value.initArray(result); }, .string => |s| { const len: i64 = @intCast(s.len); var start: i64 = if (from == .null) 0 else if (from == .integer) from.integer else return error.InvalidType; var end: i64 = if (to == .null) len else if (to == .integer) to.integer else return error.InvalidType; if (start < 0) start += len; if (end < 0) end += len; start = @max(0, @min(start, len)); end = @max(0, @min(end, len)); if (start >= end) { const duped = allocator.dupe(u8, "") catch return error.InternalError; return Value.initString(duped); } const ustart: usize = @intCast(start); const uend: usize = @intCast(end); const duped = allocator.dupe(u8, s[ustart..uend]) catch return error.InternalError; return Value.initString(duped); }, .null => return .null, else => return error.InvalidType, } } pub const CompareOp = enum { eq, ne, lt, gt, le, ge }; pub fn compare(lhs: Value, rhs: Value, op: CompareOp) OpsError!bool { const lhs_tag = lhs.kind(); const rhs_tag = rhs.kind(); if (lhs_tag != rhs_tag) { const lhs_is_number = lhs_tag == .integer or lhs_tag == .float; const rhs_is_number = rhs_tag == .integer or rhs_tag == .float; if (lhs_is_number and rhs_is_number) { return compareNumbers(lhs, rhs, op); } return error.InvalidType; } return switch (lhs) { .null => switch (op) { .eq => true, .ne => false, .lt, .gt, .le, .ge => error.Unimplemented, }, .bool => |lhs_bool| { const rhs_bool = rhs.bool; return switch (op) { .eq => lhs_bool == rhs_bool, .ne => lhs_bool != rhs_bool, .lt, .gt, .le, .ge => error.Unimplemented, }; }, .integer, .float => compareNumbers(lhs, rhs, op), .string => |lhs_str| { const rhs_str = rhs.string; const order = std.mem.order(u8, lhs_str, rhs_str); return switch (op) { .eq => order == .eq, .ne => order != .eq, .lt => order == .lt, .gt => order == .gt, .le => order == .lt or order == .eq, .ge => order == .gt or order == .eq, }; }, .array => switch (op) { .eq, .ne => error.Unimplemented, .lt, .gt, .le, .ge => error.Unimplemented, }, .object => switch (op) { .eq, .ne => error.Unimplemented, .lt, .gt, .le, .ge => error.Unimplemented, }, }; } fn compareNumbers(lhs: Value, rhs: Value, op: CompareOp) bool { const lhs_f: f64 = switch (lhs) { .integer => |i| @floatFromInt(i), .float => |f| f, else => unreachable, }; const rhs_f: f64 = switch (rhs) { .integer => |i| @floatFromInt(i), .float => |f| f, else => unreachable, }; return switch (op) { .eq => lhs_f == rhs_f, .ne => lhs_f != rhs_f, .lt => lhs_f < rhs_f, .gt => lhs_f > rhs_f, .le => lhs_f <= rhs_f, .ge => lhs_f >= rhs_f, }; } test "index array" { var arr = try Array.init(std.testing.allocator); defer arr.deinit(std.testing.allocator); try arr.append(std.testing.allocator, Value.initInteger(10)); try arr.append(std.testing.allocator, Value.initInteger(20)); try arr.append(std.testing.allocator, Value.initInteger(30)); const base = Value.initArray(arr); try std.testing.expectEqual(Value.initInteger(10), try index(base, Value.initInteger(0))); try std.testing.expectEqual(Value.initInteger(20), try index(base, Value.initInteger(1))); try std.testing.expectEqual(Value.initInteger(30), try index(base, Value.initInteger(2))); try std.testing.expectEqual(Value.null, try index(base, Value.initInteger(3))); } test "index object" { var obj = try Object.init(std.testing.allocator); defer obj.deinit(std.testing.allocator); try obj.set(std.testing.allocator, "foo", Value.initInteger(1)); try obj.set(std.testing.allocator, "bar", Value.initInteger(2)); const base = Value.initObject(obj); try std.testing.expectEqual(Value.initInteger(1), try index(base, Value.initString("foo"))); try std.testing.expectEqual(Value.initInteger(2), try index(base, Value.initString("bar"))); try std.testing.expectEqual(Value.null, try index(base, Value.initString("baz"))); } test "index null" { try std.testing.expectEqual(Value.null, try index(Value.null, Value.initInteger(0))); try std.testing.expectEqual(Value.null, try index(Value.null, Value.initString("foo"))); } test "index invalid type" { try std.testing.expectError(error.InvalidType, index(Value.initInteger(42), Value.initInteger(0))); try std.testing.expectError(error.InvalidType, index(Value.initString("foo"), Value.initInteger(0))); } test "compare integers" { try std.testing.expect(try compare(Value.initInteger(1), Value.initInteger(1), .eq)); try std.testing.expect(!try compare(Value.initInteger(1), Value.initInteger(2), .eq)); try std.testing.expect(try compare(Value.initInteger(1), Value.initInteger(2), .ne)); try std.testing.expect(try compare(Value.initInteger(1), Value.initInteger(2), .lt)); try std.testing.expect(try compare(Value.initInteger(2), Value.initInteger(1), .gt)); try std.testing.expect(try compare(Value.initInteger(1), Value.initInteger(1), .le)); try std.testing.expect(try compare(Value.initInteger(1), Value.initInteger(1), .ge)); } test "compare floats" { try std.testing.expect(try compare(Value.initFloat(1.5), Value.initFloat(1.5), .eq)); try std.testing.expect(try compare(Value.initFloat(1.5), Value.initFloat(2.5), .lt)); try std.testing.expect(try compare(Value.initFloat(2.5), Value.initFloat(1.5), .gt)); } test "compare mixed numbers" { try std.testing.expect(try compare(Value.initInteger(2), Value.initFloat(2.0), .eq)); try std.testing.expect(try compare(Value.initFloat(1.5), Value.initInteger(2), .lt)); try std.testing.expect(try compare(Value.initInteger(3), Value.initFloat(2.5), .gt)); } test "compare strings" { try std.testing.expect(try compare(Value.initString("abc"), Value.initString("abc"), .eq)); try std.testing.expect(try compare(Value.initString("abc"), Value.initString("abd"), .ne)); try std.testing.expect(try compare(Value.initString("abc"), Value.initString("abd"), .lt)); try std.testing.expect(try compare(Value.initString("abd"), Value.initString("abc"), .gt)); } test "compare booleans" { try std.testing.expect(try compare(Value.initBool(true), Value.initBool(true), .eq)); try std.testing.expect(try compare(Value.initBool(false), Value.initBool(false), .eq)); try std.testing.expect(try compare(Value.initBool(true), Value.initBool(false), .ne)); } test "compare null" { try std.testing.expect(try compare(Value.null, Value.null, .eq)); try std.testing.expect(!try compare(Value.null, Value.null, .ne)); } test "compare different types" { try std.testing.expectError(error.InvalidType, compare(Value.initInteger(1), Value.initString("1"), .eq)); try std.testing.expectError(error.InvalidType, compare(Value.null, Value.initInteger(0), .eq)); } pub fn isFalsy(value: Value) bool { return switch (value) { .null => true, .bool => |b| !b, else => false, }; } pub fn isTruthy(value: Value) bool { return !isFalsy(value); } test "isFalsy" { try std.testing.expect(isFalsy(Value.null)); try std.testing.expect(isFalsy(Value.initBool(false))); try std.testing.expect(!isFalsy(Value.initBool(true))); try std.testing.expect(!isFalsy(Value.initInteger(0))); try std.testing.expect(!isFalsy(Value.initInteger(1))); try std.testing.expect(!isFalsy(Value.initString(""))); try std.testing.expect(!isFalsy(Value.initString("hello"))); } test "isTruthy" { try std.testing.expect(!isTruthy(Value.null)); try std.testing.expect(!isTruthy(Value.initBool(false))); try std.testing.expect(isTruthy(Value.initBool(true))); try std.testing.expect(isTruthy(Value.initInteger(0))); try std.testing.expect(isTruthy(Value.initInteger(1))); try std.testing.expect(isTruthy(Value.initString(""))); try std.testing.expect(isTruthy(Value.initString("hello"))); }