350 lines
8.5 KiB
Zig
350 lines
8.5 KiB
Zig
const debug = @import("./debug.zig");
|
|
|
|
const io = @import("./io.zig");
|
|
|
|
const math = @import("./math.zig");
|
|
|
|
pub const DecimalFormat = struct {
|
|
delimiter: []const io.Byte,
|
|
positive_prefix: enum {none, plus, space},
|
|
|
|
pub const default = DecimalFormat{
|
|
.delimiter = "",
|
|
.positive_prefix = .none,
|
|
};
|
|
|
|
pub fn parse(self: DecimalFormat, utf8: []const io.Byte, comptime Decimal: type) ?Decimal {
|
|
if (utf8.len == 0) {
|
|
return null;
|
|
}
|
|
|
|
switch (@typeInfo(Decimal)) {
|
|
.Int => |int| {
|
|
var has_sign = switch (utf8[0]) {
|
|
'-', '+', ' ' => true,
|
|
else => false,
|
|
};
|
|
|
|
var result = @as(Decimal, 0);
|
|
|
|
for (@intFromBool(has_sign) .. utf8.len) |index| {
|
|
const radix = 10;
|
|
const code = utf8[index];
|
|
|
|
switch (code) {
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
|
|
const offset_code = math.checked_sub(code, '0') orelse return null;
|
|
|
|
result = math.checked_mul(result, radix) orelse return null;
|
|
result = math.checked_add(result, offset_code) orelse return null;
|
|
},
|
|
|
|
else => {
|
|
if (self.delimiter.len == 0 or !io.are_equal(self.delimiter, utf8[index ..])) {
|
|
return null;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
switch (int.signedness) {
|
|
.signed => {
|
|
return result * @as(Decimal, if (has_sign and utf8[0] == '-') -1 else 1);
|
|
},
|
|
|
|
.unsigned => {
|
|
if (has_sign and utf8[0] == '-') {
|
|
return null;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
}
|
|
},
|
|
|
|
.Float => {
|
|
var has_sign = switch (utf8[0]) {
|
|
'-', '+', ' ' => true,
|
|
else => false,
|
|
};
|
|
|
|
// "-"
|
|
if (has_sign and utf8.len == 1) {
|
|
return null;
|
|
}
|
|
|
|
const sign_offset = @intFromBool(has_sign);
|
|
var has_decimal = utf8[sign_offset] == '.';
|
|
|
|
// "-."
|
|
if (has_decimal and (utf8.len == 2)) {
|
|
return null;
|
|
}
|
|
|
|
var result = @as(Decimal, 0);
|
|
var factor = @as(Decimal, if (has_sign and utf8[0] == '-') -1 else 1);
|
|
|
|
for (utf8[sign_offset + @intFromBool(has_decimal) .. utf8.len]) |code| {
|
|
switch (code) {
|
|
'.' => {
|
|
if (has_decimal) {
|
|
return null;
|
|
}
|
|
|
|
has_decimal = true;
|
|
},
|
|
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
|
|
if (has_decimal) {
|
|
factor /= 10.0;
|
|
}
|
|
|
|
result = ((result * 10.0) + @as(Decimal, @floatFromInt(code - '0')));
|
|
},
|
|
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
return result * factor;
|
|
},
|
|
|
|
else => @compileError("`" ++ @typeName(Decimal) ++ "` cannot be formatted as a decimal string"),
|
|
}
|
|
}
|
|
|
|
pub fn print(self: DecimalFormat, writer: io.Writer, value: anytype) ?usize {
|
|
if (value == 0) {
|
|
return print_string(writer, switch (self.positive_prefix) {
|
|
.none => "0",
|
|
.plus => "+0",
|
|
.space => " 0",
|
|
});
|
|
}
|
|
|
|
const ValueType = @TypeOf(value);
|
|
|
|
switch (@typeInfo(ValueType)) {
|
|
.Int => |int| {
|
|
const radix = 10;
|
|
var buffer = [_]u8{0} ** (1 + @max(int.bits, 1));
|
|
var buffer_start = buffer.len - 1;
|
|
|
|
{
|
|
var decomposable_value = value;
|
|
|
|
while (decomposable_value != 0) : (buffer_start -= 1) {
|
|
buffer[buffer_start] = @intCast(@mod(decomposable_value, radix) + '0');
|
|
decomposable_value = @divTrunc(decomposable_value, radix);
|
|
}
|
|
}
|
|
|
|
if (int.signedness == .unsigned and value < 0) {
|
|
buffer[buffer_start] = '-';
|
|
} else {
|
|
switch (self.positive_prefix) {
|
|
.none => buffer_start += 1,
|
|
.plus => buffer[buffer_start] = '+',
|
|
.space => buffer[buffer_start] = ' ',
|
|
}
|
|
}
|
|
|
|
return print_string(writer, buffer[buffer_start ..]);
|
|
},
|
|
|
|
.Float => |float| {
|
|
var length = @as(usize, 0);
|
|
|
|
if (value < 0) {
|
|
length += print_string(writer, "-") orelse return null;
|
|
}
|
|
|
|
const Float = @TypeOf(value);
|
|
|
|
const Int = math.Int(.{
|
|
.bits = float.bits,
|
|
.signedness = .unsigned,
|
|
});
|
|
|
|
const integer = @as(Int, @intFromFloat(value));
|
|
|
|
length += self.print(writer, integer) orelse return null;
|
|
length += print_string(writer, ".") orelse return null;
|
|
length += self.print(writer, @as(Int, @intFromFloat((value - @as(Float, @floatFromInt(integer))) * 100))) orelse return null;
|
|
|
|
return length;
|
|
},
|
|
|
|
else => @compileError(unformattableMessage(ValueType)),
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const HexadecimalFormat = struct {
|
|
delimiter: []const u8 = "",
|
|
positive_prefix: enum {none, plus, space} = .none,
|
|
casing: enum {lower, upper} = .lower,
|
|
|
|
const default = HexadecimalFormat{
|
|
.delimiter = "",
|
|
.positive_prefix = .none,
|
|
.casing = .lower,
|
|
};
|
|
|
|
pub fn print(self: HexadecimalFormat, writer: io.Writer, value: anytype) ?usize {
|
|
// TODO: Implement.
|
|
_ = self;
|
|
_ = writer;
|
|
_ = value;
|
|
|
|
unreachable;
|
|
}
|
|
};
|
|
|
|
pub const SmallString = extern struct {
|
|
data: [256]u8 = [_]u8{0} ** 256,
|
|
|
|
pub const FromError = error {
|
|
InvalidUtf8,
|
|
TooBig,
|
|
};
|
|
|
|
pub fn from_units(units: []const u8) FromError!SmallString {
|
|
var self = SmallString{};
|
|
const units_range = self.data.len - 1;
|
|
|
|
if (units.len > units_range) {
|
|
return error.TooBig;
|
|
}
|
|
|
|
io.copy(self.data[0 .. units_range], units);
|
|
|
|
self.data[units_range] = @intCast(units_range - units.len);
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn to_units(self: *const SmallString) [:0]const u8 {
|
|
const units_range = self.data.len - 1;
|
|
|
|
return @ptrCast(self.data[0 .. units_range - self.data[units_range]]);
|
|
}
|
|
};
|
|
|
|
pub fn alloc_formatted(allocator: io.Allocator, comptime format: []const u8, args: anytype) io.AllocationError![]io.Byte {
|
|
const formatted_len = print_formatted(io.null_writer, format, args);
|
|
|
|
debug.assert(formatted_len != null);
|
|
|
|
const allocation = try allocator.reallocate(null, formatted_len.?);
|
|
|
|
errdefer allocator.deallocate(allocation);
|
|
|
|
{
|
|
var fixed_buffer = io.FixedBuffer{.bytes = allocation};
|
|
|
|
debug.assert(print_formatted(fixed_buffer.as_writer(), format, args) == formatted_len);
|
|
}
|
|
|
|
return allocation;
|
|
}
|
|
|
|
pub fn print_string(writer: io.Writer, utf8: []const u8) ?usize {
|
|
return writer.invoke(utf8);
|
|
}
|
|
|
|
pub fn print_formatted(writer: io.Writer, comptime format: []const u8, args: anytype) ?usize {
|
|
var length = @as(usize, 0);
|
|
|
|
switch (@typeInfo(@TypeOf(args))) {
|
|
.Struct => |arguments_struct| {
|
|
comptime var arg_index = 0;
|
|
comptime var head = 0;
|
|
comptime var tail = 0;
|
|
|
|
inline while (tail < format.len) : (tail += 1) {
|
|
if (format[tail] == '{') {
|
|
if (tail > format.len) {
|
|
@compileError("expected an idenifier after opening `{`");
|
|
}
|
|
|
|
tail += 1;
|
|
|
|
switch (format[tail]) {
|
|
'{' => {
|
|
length += print_string(writer, format[head .. (tail - 1)]) orelse return null;
|
|
tail += 1;
|
|
head = tail;
|
|
},
|
|
|
|
'}' => {
|
|
if (!arguments_struct.is_tuple) {
|
|
@compileError("all format specifiers must be named when using a named struct");
|
|
}
|
|
|
|
length += print_string(writer, args[arg_index]) orelse return null;
|
|
arg_index += 1;
|
|
tail += 1;
|
|
head = tail;
|
|
},
|
|
|
|
else => {
|
|
if (arguments_struct.is_tuple) {
|
|
@compileError("format specifiers cannot be named when using a tuple struct");
|
|
}
|
|
|
|
length += print_string(writer, format[head .. (tail - 1)]) orelse return null;
|
|
head = tail;
|
|
tail += 1;
|
|
|
|
if (tail >= format.len) {
|
|
@compileError("expected closing `}` or another `{` after opening `{`");
|
|
}
|
|
|
|
debug.assert(tail < format.len);
|
|
|
|
inline while (format[tail] != '}') {
|
|
tail += 1;
|
|
|
|
debug.assert(tail < format.len);
|
|
}
|
|
|
|
length += print_value(writer, @field(args, format[head .. tail])) orelse return null;
|
|
tail += 1;
|
|
head = tail;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
length += print_string(writer, format[head .. ]) orelse return null;
|
|
},
|
|
|
|
else => @compileError("`arguments` must be a struct type"),
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
noinline fn print_value(writer: io.Writer, value: anytype) ?usize {
|
|
const Value = @TypeOf(value);
|
|
|
|
return switch (@typeInfo(Value)) {
|
|
.Int => DecimalFormat.default.print(writer, value),
|
|
.Float => DecimalFormat.default.print(writer, value),
|
|
.ComptimeInt => print_value(writer, @as(@import("std").math.IntFittingRange(value, value), value)),
|
|
|
|
.Pointer => |pointer| switch (pointer.size) {
|
|
.Many, .C => HexadecimalFormat.default.print(writer, @intFromPtr(value)),
|
|
.One => if (pointer.child == []const u8) print_string(writer, *value) else HexadecimalFormat.default.print(writer, @intFromPtr(value)),
|
|
.Slice => if (pointer.child == u8) print_string(writer, value) else @compileError(unformattableMessage(Value)),
|
|
},
|
|
|
|
else => @compileError(unformattableMessage(Value)),
|
|
};
|
|
}
|
|
|
|
fn unformattableMessage(comptime Value: type) []const u8 {
|
|
return "type `" ++ @typeName(Value) ++ "` is not formattable with this formatter";
|
|
}
|