diff --git a/src/core/fmt.zig b/src/core/fmt.zig deleted file mode 100644 index 43a0cba..0000000 --- a/src/core/fmt.zig +++ /dev/null @@ -1,70 +0,0 @@ -const io = @import("io.zig"); - -/// -/// Number formatting modes supported by [writeInt]. -/// -pub const Radix = enum { - binary, - tinary, - quaternary, - quinary, - senary, - septenary, - octal, - nonary, - decimal, - undecimal, - duodecimal, - tridecimal, - tetradecimal, - pentadecimal, - hexadecimal, -}; - -/// -/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full -/// sequence was successfully written, otherwise `false`. -/// -/// The `radix` argument identifies which base system to format `value` as. -/// -pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) bool { - const Int = @TypeOf(value); - const type_info = @typeInfo(Int); - - switch (type_info) { - .Int => { - if (value == 0) return writer.writeByte('0'); - - // TODO: Unhardcode this as it will break with large ints. - var buffer = std.mem.zeroes([28]u8); - var buffer_count = @as(usize, 0); - var n1 = value; - - if ((type_info.Int.signedness == .signed) and (value < 0)) { - // Negative value. - n1 = -value; - buffer[0] = '-'; - buffer_count += 1; - } - - while (n1 != 0) { - const base = @enumToInt(radix); - - buffer[buffer_count] = @intCast(u8, (n1 % base) + '0'); - n1 = (n1 / base); - buffer_count += 1; - } - - for (buffer[0 .. (buffer_count / 2)]) |_, i| - std.mem.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]); - - return (writer.call(.{buffer[0 .. buffer_count]}) == buffer_count); - }, - - // Cast comptime int into known-size integer and try again. - .ComptimeInt => return writer. - writeInt(radix, @intCast(std.math.IntFittingRange(value, value), value)), - - else => @compileError("value must be of type int"), - } -} diff --git a/src/core/io.zig b/src/core/io.zig index 11c3fa1..495fd7d 100644 --- a/src/core/io.zig +++ b/src/core/io.zig @@ -1,11 +1,12 @@ +const math = @import("./math.zig"); const meta = @import("./meta.zig"); const stack = @import("./stack.zig"); -const std = @import("std"); +const testing = @import("./testing.zig"); /// /// /// -pub const Allocator = std.mem.Allocator; +pub const Allocator = @import("std").mem.Allocator; /// /// Closure that captures a reference to readable resources like block devices, memory buffers, @@ -31,6 +32,26 @@ pub fn Spliterator(comptime Element: type) type { return (self.source.len != 0); } + test "Check has data" { + var empty_spliterator = Spliterator(u8){ + .source = "", + .delimiter = "/", + }; + + try testing.expect(!empty_spliterator.hasNext()); + + var stateful_spliterator = Spliterator(u8){ + .source = "data", + .delimiter = "/", + }; + + try testing.expect(stateful_spliterator.hasNext()); + + _ = try stateful_spliterator.next(); + + try testing.expect(!stateful_spliterator.hasNext()); + } + /// /// Iterates on `self` and returns the next view of [Spliterator.source] that matches /// [Spliterator.delimiter], or `null` if there is no more data to be processed. @@ -38,7 +59,11 @@ pub fn Spliterator(comptime Element: type) type { pub fn next(self: *Self) ?[]const Element { if (!self.hasNext()) return null; - if (std.mem.indexOfPos(Element, self.source, 0, self.delimiter)) |index| { + if (findFirstOf(Element, self.source, self.delimiter, struct { + fn testEquality(this: Element, that: Element) bool { + return this == that; + } + }.testEquality)) |index| { defer self.source = self.source[(index + self.delimiter.len) .. self.source.len]; return self.source[0 .. index]; @@ -48,43 +73,43 @@ pub fn Spliterator(comptime Element: type) type { return self.source; } + + test "Iterate through data" { + // Single-character delimiter. + { + var spliterator = Spliterator(u8){ + .source = "single.character.separated.hello.world", + .delimiter = ".", + }; + + const components = [_][]const u8{"single", + "character", "separated", "hello", "world"}; + + var index = @as(usize, 0); + + while (spliterator.next()) |split| : (index += 1) { + try testing.expect(equals(u8, split, components[index])); + } + } + + // Multi-character delimiter. + { + var spliterator = Spliterator(u8){ + .source = "finding a needle in a needle stack", + .delimiter = "needle", + }; + + const components = [_][]const u8{"finding a ", " in a ", " stack"}; + var index = @as(usize, 0); + + while (spliterator.next()) |split| : (index += 1) { + try testing.expect(equals(u8, split, components[index])); + } + } + } }; } -test "Spliterating text" { - const testing = std.testing; - - // Single-character delimiter. - { - var spliterator = Spliterator(u8){ - .source = "single.character.separated.hello.world", - .delimiter = ".", - }; - - const components = [_][]const u8{"single", "character", "separated", "hello", "world"}; - var index = @as(usize, 0); - - while (spliterator.next()) |split| : (index += 1) { - try testing.expect(equals(u8, split, components[index])); - } - } - - // Multi-character delimiter. - { - var spliterator = Spliterator(u8){ - .source = "finding a needle in a needle stack", - .delimiter = "needle", - }; - - const components = [_][]const u8{"finding a ", " in a ", " stack"}; - var index = @as(usize, 0); - - while (spliterator.next()) |split| : (index += 1) { - try testing.expect(equals(u8, split, components[index])); - } - } -} - /// /// Closure that captures a reference to writable resources like block devices, memory buffers, /// network sockets, and more. @@ -102,7 +127,6 @@ pub fn begins(comptime Element: type, elements: []const Element, with: []const E test "Check memory begins with" { const bytes_sequence = &.{69, 42}; - const testing = std.testing; try testing.expect(begins(u8, &.{69, 42, 0, 89}, bytes_sequence)); try testing.expect(!begins(u8, &.{69, 89, 42, 0}, bytes_sequence)); @@ -133,11 +157,9 @@ pub fn bytesOf(pointer: anytype) switch (@typeInfo(@TypeOf(pointer))) { } test "Bytes of types" { - const testing = std.testing; - var foo: u32 = 10; - testing.expectEqual(bytesOf(&foo), 0x0a); + try testing.expect(bytesOf(&foo)[0] == 0x0a); } /// @@ -145,27 +167,38 @@ test "Bytes of types" { /// sequences, otherwise `0` if they are identical. /// pub fn compareBytes(this: []const u8, that: []const u8) isize { - var cursor: usize = 0; + const range = math.min(usize, this.len, that.len); + var index: usize = 0; - while (cursor != this.len) : (cursor += 1) { - const this_byte = this[cursor]; + while (index < range) : (index += 1) { + const difference = (this[index] - that[index]); - if (cursor != that.len) return this_byte; - - const that_byte = that[cursor]; - - if (this_byte != that_byte) return (this_byte - that_byte); + if (difference != 0) return difference; } - return 0; + return (@intCast(isize, this.len) - @intCast(isize, that.len)); } test "Compare bytes" { - const testing = std.testing; + try testing.expect(compareBytes(&.{69, 42, 0}, &.{69, 42, 0}) == 0); + try testing.expect(compareBytes(&.{69, 42, 11}, &.{69, 42}) == 1); + try testing.expect(compareBytes(&.{69, 42}, &.{69, 42, 11}) == -1); +} - try testing.expectEquals(compareBytes(&.{69, 42, 0}, &.{69, 42, 0}), 0); - try testing.expectEquals(compareBytes(&.{69, 42, 0}, &.{69, 42}), 42); - try testing.expectEquals(compareBytes(&.{69, 42}, &.{69, 42, 0}), -42); +/// +/// Copies the contents of `source` into `target` +/// +pub fn copy(comptime Element: type, target: []Element, source: []const Element) void { + for (source) |element, index| target[index] = element; +} + +test "Copy data" { + var buffer = [_]u32{0} ** 20; + const data = [_]u32{3, 20, 8000}; + + copy(u32, &buffer, &data); + + for (data) |datum, index| try testing.expect(buffer[index] == datum); } /// @@ -175,21 +208,72 @@ test "Compare bytes" { pub fn equals(comptime Element: type, this: []const Element, that: []const Element) bool { if (this.len != that.len) return false; - var i = std.mem.zeroes(usize); + var index: usize = 0; - while (i < this.len) : (i += 1) if (this[i] != that[i]) return false; + while (index < this.len) : (index += 1) if (this[index] != that[index]) return false; return true; } -test "Check memory is equals" { +test "Check memory is equal" { const bytes_sequence = &.{69, 42, 0}; - const testing = std.testing; try testing.expect(equals(u8, bytes_sequence, bytes_sequence)); try testing.expect(!equals(u8, bytes_sequence, &.{69, 42})); } +/// +/// Searches for the first instance of an `Element` equal to `needle` in `haystack`, returning its +/// index or `null` if nothing was found. +/// +pub fn findFirst(comptime Element: type, haystack: []const Element, + needle: Element, comptime testEquality: fn (Element, Element) bool) ?usize { + + for (haystack) |element, index| if (testEquality(element, needle)) return index; + + return null; +} + +test "Find first of element" { + const haystack = &.{"", "", "foo"}; + + const testEquality = struct { + fn testEquality(this: []const u8, that: []const u8) bool { + return equals(u8, this, that); + } + }.testEquality; + + try testing.expect(findFirst([]const u8, haystack, "foo", testEquality).? == 2); + try testing.expect(findFirst([]const u8, haystack, "bar", testEquality) == null); +} + +/// +/// Searches for the first instance of an `Element` sequence equal to the contents of `needle` in +/// `haystack`, returning the starting index or `null` if nothing was found. +/// +pub fn findFirstOf(comptime Element: type, haystack: []const Element, + needle: []const Element, comptime testEquality: fn (Element, Element) bool) ?usize { + + var cursor: usize = 0; + const end = (haystack.len - needle.len); + + walk_haystack: while (cursor <= end) : (cursor += 1) { + const range = (cursor + needle.len); + var index = cursor; + + while (index < range) : (index += 1) + if (testEquality(haystack[index], needle[index])) continue: walk_haystack; + + return cursor; + } + + return null; +} + +test "Find first of sequence" { + +} + /// /// Returns a deterministic hash code compiled from each byte in `bytes`. /// @@ -205,12 +289,30 @@ pub fn hashBytes(bytes: []const u8) usize { test "Hashing bytes" { const bytes_sequence = &.{69, 42, 0}; - const testing = std.testing; try testing.expect(hashBytes(bytes_sequence) == hashBytes(bytes_sequence)); try testing.expect(hashBytes(bytes_sequence) != hashBytes(&.{69, 42})); } +/// +/// Swaps the `Data` in `this` with `that`. +/// +pub fn swap(comptime Data: type, this: *Data, that: *Data) void { + const temp = this.*; + this.* = that.*; + that.* = temp; +} + +test "Data swapping" { + var a: u64 = 0; + var b: u64 = 1; + + swap(u64, &a, &b); + + try testing.expect(a == 1); + try testing.expect(b == 0); +} + /// /// Returns a [Writer] that silently consumes all given data without failure and throws it away. /// @@ -218,7 +320,7 @@ test "Hashing bytes" { /// sent somewhere for whatever reason. /// pub fn nullWriter() Writer { - return Writer.capture(std.mem.zeroes(usize), struct { + return Writer.capture(@as(usize, 0), struct { fn write(_: usize, buffer: []const u8) usize { return buffer.len; } @@ -226,11 +328,7 @@ pub fn nullWriter() Writer { } test "Null writing" { - const testing = std.testing; + const sequence = "foo"; - { - const sequence = "foo"; - - try testing.expectEqual(nullWriter().apply(sequence), sequence.len); - } + try testing.expect(nullWriter().call(sequence) == sequence.len); } diff --git a/src/core/main.zig b/src/core/main.zig index 602ea7b..785b8e5 100644 --- a/src/core/main.zig +++ b/src/core/main.zig @@ -5,12 +5,17 @@ pub const io = @import("./io.zig"); /// -/// Metaprogramming introspection and generation utilities. +/// Math types and functions with a focus on graphics-specific linear algebra. +/// +pub const math = @import("./math.zig"); + +/// +/// Metaprogramming introspection and generation. /// pub const meta = @import("./meta.zig"); /// -/// Sequential last-in first-out data structures. +/// Sequential, last-in first-out data structures. /// pub const stack = @import("./stack.zig"); @@ -19,9 +24,22 @@ pub const stack = @import("./stack.zig"); /// pub const table = @import("./table.zig"); +/// +/// Unit testing suite utilities. +/// +pub const testing = @import("./testing.zig"); + +/// +/// Unicode-encoded string analysis and processing with a focus on UTF-8 encoded text. +/// +pub const unicode = @import("./unicode.zig"); + test { _ = io; + _ = math; _ = meta; _ = stack; _ = table; + _ = testing; + _ = unicode; } diff --git a/src/core/math.zig b/src/core/math.zig new file mode 100644 index 0000000..e6f9959 --- /dev/null +++ b/src/core/math.zig @@ -0,0 +1,26 @@ + +pub const IntFittingRange = @import("std").math.IntFittingRange; + +/// +/// Returns the maximum value of `Integer`. +/// +pub fn maxInt(comptime Integer: type) Integer { + return switch (@typeInfo(Integer)) { + .Int => |info| if (info.bits == 0) 0 else + ((1 << (info.bits - @boolToInt(info.signedness == .signed))) - 1), + + else => @compileError("`" ++ @typeName(Integer) ++ "` must be an int"), + }; +} + +/// +/// Returns the lowest `Number` value between `this` and `that`. +/// +pub fn min(comptime Number: type, this: Number, that: Number) Number { + return switch (@typeInfo(Number)) { + .Int, .Float, .ComptimeInt, .ComptimeFloat => if (this < that) this else that, + + else => @compileError("`" ++ @typeName(Number) ++ + "` must be an int, float, comptime_int, or comptime_float"), + }; +} diff --git a/src/core/meta.zig b/src/core/meta.zig index f76f362..e7be22d 100644 --- a/src/core/meta.zig +++ b/src/core/meta.zig @@ -1,5 +1,3 @@ -const std = @import("std"); - /// /// Returns the return type of the function type `Fn`. /// @@ -11,70 +9,13 @@ pub fn FnReturn(comptime Fn: type) type { return type_info.Fn.return_type orelse void; } -/// -/// Returns a double-input single-output closure type where `A` represents the first input type, `B` -/// represents the second, and `Out` represents the output type, and `captures_size` represents the -/// size of the closure context. -/// -pub fn BiFunction(comptime captures_size: usize, comptime A: type, - comptime B: type, comptime Out: type) type { - - return struct { - applyErased: fn (*anyopaque, A, B) Out, - context: [captures_size]u8, - - /// - /// Function type. - /// - const Self = @This(); - - /// - /// Applies `a` and `b` to `self`, producing a result according to the current context data. - /// - pub fn apply(self: *Self, a: A, b: B) Out { - return self.applyErased(&self.context, a, b); - } - - /// - /// Creates a new [Self] by capturing the `captures` value as the context and `call` as the - /// as the behavior executed when [apply] or [applyErased] is called. - /// - /// The newly created [Self] is returned. - /// - pub fn capture(captures: anytype, comptime call: fn (@TypeOf(captures), A, B) Out) Self { - const Captures = @TypeOf(captures); - - if (@sizeOf(Captures) > captures_size) - @compileError("`captures` must be smaller than or equal to " ++ - std.fmt.comptimePrint("{d}", .{captures_size}) ++ " bytes"); - - var function = Self{ - .context = undefined, - - .applyErased = struct { - fn applyErased(erased: *anyopaque, a: A, b: B) Out { - return call(if (Captures == void) {} else @ptrCast(*Captures, - @alignCast(@alignOf(Captures), erased)).*, a, b); - } - }.applyErased, - }; - - if (captures != {}) { - @ptrCast(*Captures, @alignCast(@alignOf(Captures), &function.context)).* = captures; - } - - return function; - } - }; -} - /// /// Returns a single-input single-output closure type where `In` represents the input type, `Out` /// represents the output type, and `captures_size` represents the size of the closure context. /// pub fn Function(comptime captures_size: usize, comptime In: type, comptime Out: type) type { return struct { - applyErased: fn (*anyopaque, In) Out, + callErased: fn (*anyopaque, In) Out, context: [captures_size]u8, /// @@ -83,39 +24,39 @@ pub fn Function(comptime captures_size: usize, comptime In: type, comptime Out: const Self = @This(); /// - /// Applies `input` to `self`, producing a result according to the current context data. + /// Invokes `self` with `input`, producing a result according to the current context data. /// - pub fn apply(self: *Self, input: In) Out { - return self.applyErased(&self.context, input); + pub fn call(self: *Self, input: In) Out { + return self.callErased(&self.context, input); } /// - /// Creates a new [Self] by capturing the `captures` value as the context and `call` as the - /// as the behavior executed when [apply] or [applyErased] is called. + /// Creates a new [Self] by capturing the `captures` value as the context and `invoke` as + /// the as the behavior executed when [call] or [callErased] is called. /// /// The newly created [Self] is returned. /// - pub fn capture(captures: anytype, comptime call: fn (@TypeOf(captures), In) Out) Self { + pub fn capture(captures: anytype, comptime invoke: fn (@TypeOf(captures), In) Out) Self { const Captures = @TypeOf(captures); if (@sizeOf(Captures) > captures_size) - @compileError("`captures` must be smaller than or equal to " ++ - std.fmt.comptimePrint("{d}", .{captures_size}) ++ " bytes"); + @compileError("`captures` exceeds the size limit of the capture context"); const captures_align = @alignOf(Captures); var function = Self{ .context = undefined, - .applyErased = struct { - fn applyErased(erased: *anyopaque, input: In) Out { - return call(if (Captures == void) {} else @ptrCast(*Captures, + .callErased = struct { + fn callErased(erased: *anyopaque, input: In) Out { + return invoke(if (Captures == void) {} else @ptrCast(*Captures, @alignCast(@alignOf(Captures), erased)).*, input); } - }.applyErased, + }.callErased, }; - @ptrCast(*Captures, @alignCast(captures_align, &function.context)).* = captures; + if (Captures != void) + @ptrCast(*Captures, @alignCast(captures_align, &function.context)).* = captures; return function; } diff --git a/src/core/stack.zig b/src/core/stack.zig index 77a4e01..6caa0fb 100755 --- a/src/core/stack.zig +++ b/src/core/stack.zig @@ -1,5 +1,5 @@ const io = @import("./io.zig"); -const std = @import("std"); +const testing = @import("./testing.zig"); /// /// Returns a fixed-size stack type of `Element`s. @@ -15,42 +15,20 @@ pub fn Fixed(comptime Element: type) type { const Self = @This(); /// - /// Wraps `self` and returns it in a [io.Writer] value. - /// - /// Note that this will raise a compilation error if [Element] is not `u8`. - /// - pub fn writer(self: *Self) io.Writer { - if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++ - @typeName(Element) ++ " into a Writer"); - - return io.Writer.capture(self, struct { - fn write(stack: *Self, buffer: []const u8) usize { - stack.pushAll(buffer) catch |err| switch (err) { - error.OutOfMemory => return 0, - }; - - return buffer.len; - } - }.write); - } - - /// - /// Clears all elements from `self`. + /// Resets the number of filled items to `0`, otherwise leaving the actual memory contents + /// of the buffer untouched until it is later overwritten by following operations on it. /// pub fn clear(self: *Self) void { self.filled = 0; } /// - /// Counts and returns the number of pushed elements in `self`. + /// If `self` is filled with at least `1` value, it is decremented by `1`, otherwise leaving + /// the actual memory contents of the buffer untouched until it is later overwritten by + /// following operations on it. /// - pub fn count(self: Self) usize { - return self.filled; - } - - /// - /// Attempts to pop the tail-end of `self`, returning the element value or `null` if the - /// stack is empty. + /// The value of the element removed from the list is returned if something existed to be + /// popped, otherwise `null` if it contained no elements. /// pub fn pop(self: *Self) ?Element { if (self.filled == 0) return null; @@ -74,40 +52,92 @@ pub fn Fixed(comptime Element: type) type { /// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it /// failed. /// - pub fn pushAll(self: *Self, elements: []const u8) PushError!void { + pub fn pushAll(self: *Self, elements: []const Element) PushError!void { const filled = (self.filled + elements.len); if (filled > self.buffer.len) return error.OutOfMemory; - std.mem.copy(u8, self.buffer[self.filled ..], elements); + io.copy(Element, self.buffer[self.filled ..], elements); self.filled = filled; } }; } +test "Fixed stack of string literals" { + const default_value = ""; + var buffer = [_][]const u8{default_value} ** 4; + var shopping_list = Fixed([]const u8){.buffer = &buffer}; + + // Pop empty stack. + { + try testing.expect(shopping_list.pop() == null); + try testing.expect(shopping_list.filled == 0); + try testing.expect(shopping_list.buffer.ptr == &buffer); + try testing.expect(shopping_list.buffer.len == buffer.len); + + for (shopping_list.buffer) |item| + try testing.expect(io.equals(u8, item, default_value)); + } + + // Push single element. + { + try shopping_list.push("milk"); + try testing.expect(shopping_list.filled == 1); + try testing.expect(shopping_list.buffer.ptr == &buffer); + try testing.expect(shopping_list.buffer.len == buffer.len); + try testing.expect(io.equals(u8, shopping_list.buffer[0], "milk")); + + for (shopping_list.buffer[1 ..]) |item| + try testing.expect(io.equals(u8, item, default_value)); + + // TODO: Test stack overflow. + } + + // Pop single element. + { + try testing.expect(io.equals(u8, shopping_list.pop().?, "milk")); + try testing.expect(shopping_list.filled == 0); + try testing.expect(shopping_list.buffer.ptr == &buffer); + try testing.expect(shopping_list.buffer.len == buffer.len); + try testing.expect(io.equals(u8, shopping_list.buffer[0], "milk")); + + for (shopping_list.buffer[1 ..]) |item| + try testing.expect(io.equals(u8, item, default_value)); + } + + // TODO: Multiple elements. + // TODO: Clear elements. +} + /// /// Potential errors that may occur while trying to push one or more elements into a stack. /// -pub const PushError = std.mem.Allocator.Error; +pub const PushError = io.Allocator.Error; -test "Fixed stack manipulation" { - const testing = std.testing; - var buffer = std.mem.zeroes([4]u8); - var stack = Fixed(u8){.buffer = &buffer}; +/// +/// Returns an [io.Writer] wrapping `fixed_stack`. +/// +/// Writing to the returned [io.Writer] will push values to the underlying [Fixed] stack instance +/// referenced by `fixed_stack` until it is full. +/// +pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer { + return io.Writer.capture(fixed_stack, struct { + fn write(stack: *Fixed(u8), buffer: []const u8) usize { + stack.pushAll(buffer) catch |err| switch (err) { + error.OutOfMemory => return 0, + }; - try testing.expectEqual(stack.count(), 0); - try testing.expectEqual(stack.pop(), null); - try stack.push(69); - try testing.expectEqual(stack.count(), 1); - try testing.expectEqual(stack.pop(), 69); - try stack.pushAll(&.{42, 10, 95, 0}); - try testing.expectEqual(stack.count(), 4); - try testing.expectError(PushError.OutOfMemory, stack.push(1)); - try testing.expectError(PushError.OutOfMemory, stack.pushAll(&.{1, 11, 11})); - - stack.clear(); - - try testing.expectEqual(stack.count(), 0); - try testing.expectEqual(stack.writer().apply(&.{0, 0, 0, 0}), 4); + return buffer.len; + } + }.write); +} + +test "Fixed writer" { + var buffer = [_]u8{0} ** 4; + var sequence_stack = Fixed(u8){.buffer = &buffer}; + const sequence_data = [_]u8{8, 16, 32, 64}; + + try testing.expect(fixedWriter(&sequence_stack).call(&sequence_data) == sequence_data.len); + try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data)); } diff --git a/src/core/testing.zig b/src/core/testing.zig new file mode 100644 index 0000000..1881378 --- /dev/null +++ b/src/core/testing.zig @@ -0,0 +1,18 @@ +/// +/// [TestError.UnexpectedResult] occurs when a conditional that should have been `true` was actually +/// `false`. +/// +pub const TestError = error { + UnexpectedResult, +}; + +/// +/// Returns a [TestError] if `ok` is false. +/// +pub fn expect(ok: bool) TestError!void { + if (!ok) return error.UnexpectedResult; +} + +// TODO: Implement tests. + +pub const expectError = @import("std").testing.expectError; diff --git a/src/core/unicode.zig b/src/core/unicode.zig new file mode 100644 index 0000000..6e89678 --- /dev/null +++ b/src/core/unicode.zig @@ -0,0 +1,84 @@ +const io = @import("./io.zig"); +const math = @import("./math.zig"); + +/// +/// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write +/// the entirety of a the requested print operation. +/// +pub const PrintError = error { + WriteFailure, +}; + +/// +/// Number formatting modes supported by [printInt]. +/// +pub const Radix = enum { + binary, + tinary, + quaternary, + quinary, + senary, + septenary, + octal, + nonary, + decimal, + undecimal, + duodecimal, + tridecimal, + tetradecimal, + pentadecimal, + hexadecimal, +}; + +/// +/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full +/// sequence was successfully written, otherwise `false`. +/// +/// The `radix` argument identifies which base system to format `value` as. +/// +pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void { + const Int = @TypeOf(value); + + switch (@typeInfo(Int)) { + .Int => |int_info| { + if (value == 0) return writer.apply("0"); + + const base = @enumToInt(radix); + const is_signed = (int_info.signedness == .signed); + + var buffer = [_]u8{0} ** (math.ceil(math.log(math. + maxInt(Int), base)) + @boolToInt(is_signed)); + + var buffer_count: usize = 0; + var n1 = value; + + if (is_signed and (value < 0)) { + // Negative value. + n1 = -value; + buffer[0] = '-'; + buffer_count += 1; + } + + while (n1 != 0) { + buffer[buffer_count] = @intCast(u8, (n1 % base) + '0'); + n1 = (n1 / base); + buffer_count += 1; + } + + for (buffer[0 .. (buffer_count / 2)]) |_, i| + io.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]); + + if (writer.call(buffer[0 .. buffer_count]) != buffer_count) return error.WriteFailure; + }, + + // Cast comptime int into known-size integer and try again. + .ComptimeInt => return printInt(writer, radix, + @intCast(math.IntFittingRange(value, value), value)), + + else => @compileError("`value` must be of type int or comptime_int"), + } +} + +test "Print 64-bit signed integer" { + // TODO: implement. +}