1//! String formatting and parsing.
2
3const std = @import("std");
4const builtin = @import("builtin");
5const extras = @import("extras");
6const nio = @import("./nio.zig");
7
8/// Renders fmt string with args, calling `writer` with slices of bytes.
9/// If `writer` returns an error, the error is returned from `format` and
10/// `writer` is not called again.
11///
12/// The format string must be comptime-known and may contain placeholders following
13/// this format:
14/// `{[argument][specifier]:[fill][alignment][width].[precision]}`
15///
16/// Above, each word including its surrounding [ and ] is a parameter which you have to replace with something:
17///
18/// - *argument* is either the numeric index or the field name of the argument that should be inserted
19/// - when using a field name, you are required to enclose the field name (an identifier) in square
20/// brackets, e.g. {[score]...} as opposed to the numeric index form which can be written e.g. {2...}
21/// - *specifier* is a type-dependent formatting option that determines how a type should formatted (see below)
22/// - *fill* is a single unicode codepoint which is used to pad the formatted text
23/// - *alignment* is one of the three bytes '<', '^', or '>' to make the text left-, center-, or right-aligned, respectively
24/// - *width* is the total width of the field in unicode codepoints
25/// - *precision* specifies how many decimals a formatted number should have
26///
27/// Note that most of the parameters are optional and may be omitted. Also you can leave out separators like `:` and `.` when
28/// all parameters after the separator are omitted.
29/// Only exception is the *fill* parameter. If a non-zero *fill* character is required at the same time as *width* is specified,
30/// one has to specify *alignment* as well, as otherwise the digit following `:` is interpreted as *width*, not *fill*.
31///
32/// The *specifier* has several options for types:
33/// - `x` and `X`: output numeric value in hexadecimal notation
34/// - `s`:
35/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination
36/// - for slices of u8, print the entire slice as a string without zero-termination
37/// - `e`: output floating point value in scientific notation
38/// - `d`: output numeric value in decimal notation
39/// - `b`: output integer value in binary notation
40/// - `o`: output integer value in octal notation
41/// - `c`: output integer as an ASCII character. Integer type must have 8 bits at max.
42/// - `u`: output integer as an UTF-8 sequence. Integer type must have 21 bits at max.
43/// - `?`: output optional value as either the unwrapped value, or `null`; may be followed by a format specifier for the underlying value.
44/// - `!`: output error union value as either the unwrapped value, or the formatted error value; may be followed by a format specifier for the underlying value.
45/// - `*`: output the address of the value instead of the value itself.
46/// - `any`: output a value of any type using its default format.
47///
48/// If a formatted user type contains a function of the type
49/// ```
50/// fn format(value: ?, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void
51/// ```
52/// with `?` being the type formatted, this function will be called instead of the default implementation.
53/// This allows user types to be formatted in a logical manner instead of dumping all fields of the type.
54///
55/// A user type may be a `struct`, `vector`, `union` or `enum` type.
56///
57/// To print literal curly braces, escape them by writing them twice, e.g. `{{` or `}}`.
58pub fn format(writer: anytype, comptime fmt: []const u8, args: anytype) !void {
59 const ArgsType = @TypeOf(args);
60 const args_type_info = @typeInfo(ArgsType);
61 if (args_type_info != .@"struct") {
62 @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
63 }
64
65 const fields_info = args_type_info.@"struct".fields;
66 if (fields_info.len > max_format_args) {
67 @compileError("32 arguments max are supported per format call");
68 }
69
70 @setEvalBranchQuota(std.math.maxInt(u32));
71 comptime var arg_state: ArgState = .{ .args_len = fields_info.len };
72 comptime var i = 0;
73 comptime var literal: []const u8 = "";
74 inline while (true) {
75 const start_index = i;
76
77 inline while (i < fmt.len) : (i += 1) {
78 switch (fmt[i]) {
79 '{', '}' => break,
80 else => {},
81 }
82 }
83
84 comptime var end_index = i;
85 comptime var unescape_brace = false;
86
87 // Handle {{ and }}, those are un-escaped as single braces
88 if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) {
89 unescape_brace = true;
90 // Make the first brace part of the literal...
91 end_index += 1;
92 // ...and skip both
93 i += 2;
94 }
95
96 literal = literal ++ fmt[start_index..end_index];
97
98 // We've already skipped the other brace, restart the loop
99 if (unescape_brace) continue;
100
101 // Write out the literal
102 if (literal.len != 0) {
103 try writer.writeAll(literal);
104 literal = "";
105 }
106
107 if (i >= fmt.len) break;
108
109 if (fmt[i] == '}') {
110 @compileError("missing opening {");
111 }
112
113 // Get past the {
114 comptime std.debug.assert(fmt[i] == '{');
115 i += 1;
116
117 const fmt_begin = i;
118 // Find the closing brace
119 inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {}
120 const fmt_end = i;
121
122 if (i >= fmt.len) {
123 @compileError("missing closing }");
124 }
125
126 // Get past the }
127 comptime std.debug.assert(fmt[i] == '}');
128 i += 1;
129
130 const placeholder = comptime Placeholder.parse(fmt[fmt_begin..fmt_end].*);
131 const arg_pos = comptime switch (placeholder.arg) {
132 .none => null,
133 .number => |pos| pos,
134 .named => |arg_name| std.meta.fieldIndex(ArgsType, arg_name) orelse @compileError("no argument with name '" ++ arg_name ++ "'"),
135 };
136
137 const width = switch (placeholder.width) {
138 .none => null,
139 .number => |v| v,
140 .named => |arg_name| blk: {
141 const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse @compileError("no argument with name '" ++ arg_name ++ "'");
142 _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments");
143 break :blk @field(args, arg_name);
144 },
145 };
146
147 const precision = switch (placeholder.precision) {
148 .none => null,
149 .number => |v| v,
150 .named => |arg_name| blk: {
151 const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse @compileError("no argument with name '" ++ arg_name ++ "'");
152 _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments");
153 break :blk @field(args, arg_name);
154 },
155 };
156
157 const arg_to_print = comptime arg_state.nextArg(arg_pos) orelse @compileError("too few arguments");
158
159 try formatType(
160 @field(args, fields_info[arg_to_print].name),
161 placeholder.specifier_arg,
162 .{
163 .fill = placeholder.fill,
164 .alignment = placeholder.alignment,
165 .width = width,
166 .precision = precision,
167 },
168 writer,
169 std.options.fmt_max_depth,
170 );
171 }
172
173 if (comptime arg_state.hasUnusedArgs()) {
174 const missing_count = arg_state.args_len - @popCount(arg_state.used_args);
175 switch (missing_count) {
176 0 => unreachable,
177 1 => @compileError("unused argument in '" ++ fmt ++ "'"),
178 else => @compileError(comptimePrint("{d}", .{missing_count}) ++ " unused arguments in '" ++ fmt ++ "'"),
179 }
180 }
181}
182
183pub const FormatOptions = struct {
184 precision: ?usize = null,
185 width: ?usize = null,
186 alignment: Alignment = default_alignment,
187 fill: u21 = default_fill_char,
188};
189
190pub const Alignment = enum { left, center, right };
191pub const Case = enum { lower, upper };
192
193const default_max_depth = 3;
194const default_alignment = .right;
195const default_fill_char = ' ';
196
197const ArgSetType = u32;
198const max_format_args = @typeInfo(ArgSetType).int.bits;
199
200pub const ArgState = struct {
201 next_arg: usize = 0,
202 used_args: ArgSetType = 0,
203 args_len: usize,
204
205 fn hasUnusedArgs(self: *@This()) bool {
206 return @popCount(self.used_args) != self.args_len;
207 }
208
209 fn nextArg(self: *@This(), arg_index: ?usize) ?usize {
210 const next_index = arg_index orelse init: {
211 const arg = self.next_arg;
212 self.next_arg += 1;
213 break :init arg;
214 };
215
216 if (next_index >= self.args_len) {
217 return null;
218 }
219
220 // Mark this argument as used
221 self.used_args |= @as(ArgSetType, 1) << @as(u5, @intCast(next_index));
222 return next_index;
223 }
224};
225
226pub const Placeholder = struct {
227 specifier_arg: []const u8,
228 fill: u21,
229 alignment: Alignment,
230 arg: Specifier,
231 width: Specifier,
232 precision: Specifier,
233
234 fn parse(comptime str: anytype) Placeholder {
235 const view = std.unicode.Utf8View.initComptime(&str);
236 comptime var parser = Parser{
237 .iter = view.iterator(),
238 };
239
240 // Parse the positional argument number
241 const arg = comptime parser.specifier() catch |err| @compileError(@errorName(err));
242
243 // Parse the format specifier
244 const specifier_arg = comptime parser.until(':');
245
246 // Skip the colon, if present
247 if (comptime parser.char()) |ch| {
248 if (ch != ':') {
249 @compileError("expected : or }, found '" ++ std.unicode.utf8EncodeComptime(ch) ++ "'");
250 }
251 }
252
253 // Parse the fill character, if present.
254 // When the width field is also specified, the fill character must
255 // be followed by an alignment specifier, unless it's '0' (zero)
256 // (in which case it's handled as part of the width specifier)
257 var fill: ?u21 = comptime if (parser.peek(1)) |ch|
258 switch (ch) {
259 '<', '^', '>' => parser.char(),
260 else => null,
261 }
262 else
263 null;
264
265 // Parse the alignment parameter
266 const alignment: ?Alignment = comptime if (parser.peek(0)) |ch| init: {
267 switch (ch) {
268 '<', '^', '>' => {
269 // consume the character
270 break :init switch (parser.char().?) {
271 '<' => .left,
272 '^' => .center,
273 else => .right,
274 };
275 },
276 else => break :init null,
277 }
278 } else null;
279
280 // When none of the fill character and the alignment specifier have
281 // been provided, check whether the width starts with a zero.
282 if (fill == null and alignment == null) {
283 fill = comptime if (parser.peek(0) == '0') '0' else null;
284 }
285
286 // Parse the width parameter
287 const width = comptime parser.specifier() catch |err| @compileError(@errorName(err));
288
289 // Skip the dot, if present
290 if (comptime parser.char()) |ch| {
291 if (ch != '.') {
292 @compileError("expected . or }, found '" ++ std.unicode.utf8EncodeComptime(ch) ++ "'");
293 }
294 }
295
296 // Parse the precision parameter
297 const precision = comptime parser.specifier() catch |err| @compileError(@errorName(err));
298
299 if (comptime parser.char()) |ch| {
300 @compileError("extraneous trailing character '" ++ std.unicode.utf8EncodeComptime(ch) ++ "'");
301 }
302
303 return Placeholder{
304 .specifier_arg = cacheString(specifier_arg[0..specifier_arg.len].*),
305 .fill = fill orelse default_fill_char,
306 .alignment = alignment orelse default_alignment,
307 .arg = arg,
308 .width = width,
309 .precision = precision,
310 };
311 }
312};
313
314pub const Specifier = union(enum) {
315 none,
316 number: usize,
317 named: []const u8,
318};
319
320/// A stream based parser for format strings.
321///
322/// Allows to implement formatters compatible with std.fmt without replicating
323/// the standard library behavior.
324pub const Parser = struct {
325 iter: std.unicode.Utf8Iterator,
326
327 // Returns a decimal number or null if the current character is not a
328 // digit
329 fn number(self: *@This()) ?usize {
330 var r: ?usize = null;
331
332 while (self.peek(0)) |code_point| {
333 switch (code_point) {
334 '0'...'9' => {
335 if (r == null) r = 0;
336 r.? *= 10;
337 r.? += code_point - '0';
338 },
339 else => break,
340 }
341 _ = self.iter.nextCodepoint();
342 }
343
344 return r;
345 }
346
347 // Returns a substring of the input starting from the current position
348 // and ending where `ch` is found or until the end if not found
349 fn until(self: *@This(), ch: u21) []const u8 {
350 const start = self.iter.i;
351 while (self.peek(0)) |code_point| {
352 if (code_point == ch)
353 break;
354 _ = self.iter.nextCodepoint();
355 }
356 return self.iter.bytes[start..self.iter.i];
357 }
358
359 // Returns the character pointed to by the iterator if available, or
360 // null otherwise
361 fn char(self: *@This()) ?u21 {
362 if (self.iter.nextCodepoint()) |code_point| {
363 return code_point;
364 }
365 return null;
366 }
367
368 // Returns true if the iterator points to an existing character and
369 // false otherwise
370 fn maybe(self: *@This(), val: u21) bool {
371 if (self.peek(0) == val) {
372 _ = self.iter.nextCodepoint();
373 return true;
374 }
375 return false;
376 }
377
378 // Returns a decimal number or null if the current character is not a
379 // digit
380 fn specifier(self: *@This()) !Specifier {
381 if (self.maybe('[')) {
382 const arg_name = self.until(']');
383
384 if (!self.maybe(']'))
385 return @field(anyerror, "Expected closing ]");
386
387 return Specifier{ .named = arg_name };
388 }
389 if (self.number()) |i|
390 return Specifier{ .number = i };
391
392 return Specifier{ .none = {} };
393 }
394
395 // Returns the n-th next character or null if that's past the end
396 fn peek(self: *@This(), n: usize) ?u21 {
397 const original_i = self.iter.i;
398 defer self.iter.i = original_i;
399
400 var i: usize = 0;
401 var code_point: ?u21 = null;
402 while (i <= n) : (i += 1) {
403 code_point = self.iter.nextCodepoint();
404 if (code_point == null) return null;
405 }
406 return code_point;
407 }
408};
409
410fn cacheString(str: anytype) []const u8 {
411 return &str;
412}
413
414fn formatType(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype, max_depth: usize) extras.Pointee(@TypeOf(writer)).WriteError!void {
415 const T = @TypeOf(value);
416 const actual_fmt = comptime if (std.mem.eql(u8, fmt, "any"))
417 defaultSpec(T)
418 else if (fmt.len != 0 and (fmt[0] == '?' or fmt[0] == '!')) switch (@typeInfo(T)) {
419 .optional, .error_union => fmt,
420 else => stripOptionalOrErrorUnionSpec(fmt),
421 } else fmt;
422
423 if (comptime std.mem.eql(u8, actual_fmt, "*")) {
424 return formatAddress(value, options, writer);
425 }
426
427 if (comptime std.meta.hasMethod(T, "format") and @typeInfo(@TypeOf(T.format)).@"fn".params[1].type.? != *std.Io.Writer) {
428 @compileError("fix this: " ++ @typeName(T));
429 }
430 if (comptime std.meta.hasMethod(T, "nprint")) {
431 return value.nprint(writer);
432 }
433
434 switch (@typeInfo(T)) {
435 .comptime_int, .int, .comptime_float, .float => {
436 return formatValue(value, actual_fmt, options, writer);
437 },
438 .void => {
439 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
440 return formatBuf("void", options, writer);
441 },
442 .bool => {
443 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
444 return formatBuf(if (value) "true" else "false", options, writer);
445 },
446 .optional => {
447 if (actual_fmt.len == 0 or actual_fmt[0] != '?') @compileError("cannot format optional without a specifier (i.e. {?} or {any})");
448 const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
449 if (value) |payload| {
450 return formatType(payload, remaining_fmt, options, writer, max_depth);
451 } else {
452 return formatBuf("null", options, writer);
453 }
454 },
455 .error_union => {
456 if (actual_fmt.len == 0 or actual_fmt[0] != '!') @compileError("cannot format error union without a specifier (i.e. {!} or {any})");
457 const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
458 if (value) |payload| {
459 return formatType(payload, remaining_fmt, options, writer, max_depth);
460 } else |err| {
461 return formatType(err, "", options, writer, max_depth);
462 }
463 },
464 .error_set => {
465 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
466 try writer.writeAll("error.");
467 return writer.writeAll(@errorName(value));
468 },
469 .@"enum" => |enumInfo| {
470 if (comptime std.mem.eql(u8, actual_fmt, "d")) {
471 try formatInt(@intFromEnum(value), 10, .lower, options, writer);
472 return;
473 }
474 if (comptime std.mem.eql(u8, actual_fmt, "s") and enumInfo.is_exhaustive) {
475 try writer.writeAll(@tagName(value));
476 return;
477 }
478 },
479 .@"union" => |info| {
480 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
481 try writer.writeAll(@typeName(T));
482 if (max_depth == 0) {
483 return writer.writeAll("{ ... }");
484 }
485 if (info.tag_type) |UnionTagType| {
486 try writer.writeAll("{ .");
487 try writer.writeAll(@tagName(@as(UnionTagType, value)));
488 try writer.writeAll(" = ");
489 inline for (info.fields) |u_field| {
490 if (value == @field(UnionTagType, u_field.name)) {
491 try formatType(@field(value, u_field.name), "any", options, writer, max_depth - 1);
492 }
493 }
494 try writer.writeAll(" }");
495 } else {
496 try format(writer, "@{x}", .{@intFromPtr(&value)});
497 }
498 return;
499 },
500 .pointer => |ptr_info| switch (ptr_info.size) {
501 .one => switch (@typeInfo(ptr_info.child)) {
502 .array, .@"enum", .@"union", .@"struct" => {
503 return formatType(value.*, actual_fmt, options, writer, max_depth);
504 },
505 else => return format(writer, "{s}@{x}", .{ @typeName(ptr_info.child), @intFromPtr(value) }),
506 },
507 .many, .c => {
508 if (actual_fmt.len == 0)
509 @compileError("cannot format pointer without a specifier (i.e. {s} or {*})");
510 if (ptr_info.sentinel() != null) {
511 return formatType(std.mem.span(value), actual_fmt, options, writer, max_depth);
512 }
513 if (actual_fmt[0] == 's' and ptr_info.child == u8) {
514 return formatBuf(std.mem.span(value), options, writer);
515 }
516 },
517 .slice => {
518 if (actual_fmt.len == 0) {
519 @compileError("cannot format slice without a specifier (i.e. {s} or {any})");
520 }
521 if (max_depth == 0) {
522 return writer.writeAll("{ ... }");
523 }
524 if (actual_fmt[0] == 's' and ptr_info.child == u8) {
525 return formatBuf(value, options, writer);
526 }
527 try writer.writeAll("{ ");
528 for (value, 0..) |elem, i| {
529 try formatType(elem, actual_fmt, options, writer, max_depth - 1);
530 if (i != value.len - 1) {
531 try writer.writeAll(", ");
532 }
533 }
534 try writer.writeAll(" }");
535 return;
536 },
537 },
538 .array => |info| {
539 if (actual_fmt.len == 0) {
540 @compileError("cannot format array without a specifier (i.e. {s} or {any})");
541 }
542 if (max_depth == 0) {
543 return writer.writeAll("{ ... }");
544 }
545 if (actual_fmt[0] == 's' and info.child == u8) {
546 return formatBuf(&value, options, writer);
547 }
548 try writer.writeAll("{ ");
549 for (value, 0..) |elem, i| {
550 try formatType(elem, actual_fmt, options, writer, max_depth - 1);
551 if (i < value.len - 1) {
552 try writer.writeAll(", ");
553 }
554 }
555 try writer.writeAll(" }");
556 return;
557 },
558 .vector => |info| {
559 if (max_depth == 0) {
560 return writer.writeAll("{ ... }");
561 }
562 try writer.writeAll("{ ");
563 var i: usize = 0;
564 while (i < info.len) : (i += 1) {
565 try formatType(value[i], actual_fmt, options, writer, max_depth - 1);
566 if (i < info.len - 1) {
567 try writer.writeAll(", ");
568 }
569 }
570 try writer.writeAll(" }");
571 return;
572 },
573 .@"fn" => @compileError("unable to format function body type, use '*const " ++ @typeName(T) ++ "' for a function pointer type"),
574 .type => {
575 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
576 return formatBuf(@typeName(value), options, writer);
577 },
578 .enum_literal => {
579 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
580 const buffer = [_]u8{'.'} ++ @tagName(value);
581 return formatBuf(buffer, options, writer);
582 },
583 .null => {
584 if (actual_fmt.len != 0) invalidFmtError(fmt, value);
585 return formatBuf("null", options, writer);
586 },
587 else => @compileError("unable to format type '" ++ @typeName(T) ++ "'"),
588 }
589 @compileError("unable to format type '" ++ @typeName(T) ++ "' with specifier '" ++ actual_fmt ++ "'");
590}
591
592pub inline fn comptimePrint(comptime fmt: []const u8, args: anytype) *const [count(fmt, args):0]u8 {
593 comptime {
594 var buf: [count(fmt, args):0]u8 = undefined;
595 _ = bufPrint(&buf, fmt, args) catch unreachable;
596 buf[buf.len] = 0;
597 const final = buf;
598 return &final;
599 }
600}
601
602fn defaultSpec(comptime T: type) [:0]const u8 {
603 switch (@typeInfo(T)) {
604 .array, .vector => return "any",
605 .pointer => |ptr_info| switch (ptr_info.size) {
606 .one => switch (@typeInfo(ptr_info.child)) {
607 .array => return "any",
608 else => {},
609 },
610 .many, .c => return "*",
611 .slice => return "any",
612 },
613 .optional => |info| return "?" ++ defaultSpec(info.child),
614 .error_union => |info| return "!" ++ defaultSpec(info.payload),
615 else => {},
616 }
617 return "";
618}
619
620fn stripOptionalOrErrorUnionSpec(comptime fmt: []const u8) []const u8 {
621 return if (std.mem.eql(u8, fmt[1..], "any"))
622 "any"
623 else
624 fmt[1..];
625}
626
627fn formatAddress(value: anytype, options: FormatOptions, writer: anytype) @TypeOf(writer).Error!void {
628 _ = options;
629 const T = @TypeOf(value);
630
631 switch (@typeInfo(T)) {
632 .pointer => |info| {
633 try writer.writeAll(@typeName(info.child) ++ "@");
634 if (info.size == .slice)
635 try formatInt(@intFromPtr(value.ptr), 16, .lower, FormatOptions{}, writer)
636 else
637 try formatInt(@intFromPtr(value), 16, .lower, FormatOptions{}, writer);
638 return;
639 },
640 .optional => |info| {
641 if (@typeInfo(info.child) == .pointer) {
642 try writer.writeAll(@typeName(info.child) ++ "@");
643 try formatInt(@intFromPtr(value), 16, .lower, FormatOptions{}, writer);
644 return;
645 }
646 },
647 else => {},
648 }
649
650 @compileError("cannot format non-pointer type " ++ @typeName(T) ++ " with * specifier");
651}
652
653pub fn formatInt(value: anytype, base: u8, case: Case, options: FormatOptions, writer: anytype) !void {
654 std.debug.assert(base >= 2);
655
656 const int_value = if (@TypeOf(value) == comptime_int) blk: {
657 const Int = std.math.IntFittingRange(value, value);
658 break :blk @as(Int, value);
659 } else value;
660
661 const value_info = @typeInfo(@TypeOf(int_value)).int;
662
663 // The type must have the same size as `base` or be wider in order for the
664 // division to work
665 const min_int_bits = comptime @max(value_info.bits, 8);
666 const MinInt = std.meta.Int(.unsigned, min_int_bits);
667
668 const abs_value = @abs(int_value);
669 // The worst case in terms of space needed is base 2, plus 1 for the sign
670 var buf: [1 + @max(@as(comptime_int, value_info.bits), 1)]u8 = undefined;
671
672 var a: MinInt = abs_value;
673 var index: usize = buf.len;
674
675 if (base == 10) {
676 while (a >= 100) : (a = @divTrunc(a, 100)) {
677 index -= 2;
678 buf[index..][0..2].* = digits2(@intCast(a % 100));
679 }
680
681 if (a < 10) {
682 index -= 1;
683 buf[index] = '0' + @as(u8, @intCast(a));
684 } else {
685 index -= 2;
686 buf[index..][0..2].* = digits2(@intCast(a));
687 }
688 } else {
689 while (true) {
690 const digit = a % base;
691 index -= 1;
692 buf[index] = digitToChar(@intCast(digit), case);
693 a /= base;
694 if (a == 0) break;
695 }
696 }
697
698 if (value_info.signedness == .signed) {
699 if (value < 0) {
700 // Negative integer
701 index -= 1;
702 buf[index] = '-';
703 } else if (options.width == null or options.width.? == 0) {
704 // Positive integer, omit the plus sign
705 } else {
706 // Positive integer
707 index -= 1;
708 buf[index] = '+';
709 }
710 }
711
712 return formatBuf(buf[index..], options, writer);
713}
714
715fn formatValue(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype) !void {
716 const T = @TypeOf(value);
717 switch (@typeInfo(T)) {
718 .float, .comptime_float => return formatFloatValue(value, fmt, options, writer),
719 .int, .comptime_int => return formatIntValue(value, fmt, options, writer),
720 .bool => return formatBuf(if (value) "true" else "false", options, writer),
721 else => comptime unreachable,
722 }
723}
724
725fn invalidFmtError(comptime fmt: []const u8, value: anytype) void {
726 @compileError("invalid format string '" ++ fmt ++ "' for type '" ++ @typeName(@TypeOf(value)) ++ "'");
727}
728
729fn formatBuf(buf: []const u8, options: FormatOptions, writer: anytype) !void {
730 if (options.width) |min_width| {
731 // In case of error assume the buffer content is ASCII-encoded
732 const width = std.unicode.utf8CountCodepoints(buf) catch buf.len;
733 const padding = if (width < min_width) min_width - width else 0;
734
735 if (padding == 0) return writer.writeAll(buf);
736
737 var fill_buffer: [4]u8 = undefined;
738 const fill_utf8 = if (std.unicode.utf8Encode(options.fill, &fill_buffer)) |len|
739 fill_buffer[0..len]
740 else |err| switch (err) {
741 error.Utf8CannotEncodeSurrogateHalf,
742 error.CodepointTooLarge,
743 => &std.unicode.utf8EncodeComptime(std.unicode.replacement_character),
744 };
745 switch (options.alignment) {
746 .left => {
747 try writer.writeAll(buf);
748 try writer.writeNTimes(fill_utf8, padding);
749 },
750 .center => {
751 const left_padding = padding / 2;
752 const right_padding = (padding + 1) / 2;
753 try writer.writeNTimes(fill_utf8, left_padding);
754 try writer.writeAll(buf);
755 try writer.writeNTimes(fill_utf8, right_padding);
756 },
757 .right => {
758 try writer.writeNTimes(fill_utf8, padding);
759 try writer.writeAll(buf);
760 },
761 }
762 } else {
763 // Fast path, avoid counting the number of codepoints
764 try writer.writeAll(buf);
765 }
766}
767
768/// Count the characters needed for format. Useful for preallocating memory
769fn count(comptime fmt: []const u8, args: anytype) u64 {
770 var counting_writer: nio.CountingWriter(nio.NullWriter) = .init(.{});
771 format(&counting_writer, fmt, args) catch unreachable;
772 return counting_writer.bytes_written;
773}
774
775/// Print a Formatter string into `buf`. Actually just a thin wrapper around `format` and `fixedBufferStream`.
776/// Returns a slice of the bytes printed to.
777pub fn bufPrint(buf: []u8, comptime fmt: []const u8, args: anytype) ![]u8 {
778 var fbs: nio.FixedBufferStream([]u8) = .init(buf);
779 format(&fbs, fmt, args) catch |err| switch (err) {
780 error.NoSpaceLeft => return error.NoSpaceLeft,
781 else => unreachable,
782 };
783 return fbs.written();
784}
785pub fn bufPrintZ(buf: []u8, comptime fmt: []const u8, args: anytype) ![:0]u8 {
786 const result = try bufPrint(buf, fmt ++ "\x00", args);
787 return result[0 .. result.len - 1 :0];
788}
789
790const digits2_alphabet = blk: {
791 var data: [200]u8 = @splat(0);
792 for (0..10) |m| {
793 for (0..10) |n| {
794 data[(m * 10 + n) * 2 + 0] = m + '0';
795 data[(m * 10 + n) * 2 + 1] = n + '0';
796 }
797 }
798 const result = data;
799 break :blk result;
800};
801
802/// Converts values in the range [0, 100) to a base 10 string.
803pub fn digits2(value: u8) [2]u8 {
804 return digits2_alphabet[value * 2 ..][0..2].*;
805}
806
807pub fn digitToChar(digit: u8, case: Case) u8 {
808 return switch (digit) {
809 0...9 => digit + '0',
810 10...35 => digit + (if (case == .upper) @as(u8, 'A') else @as(u8, 'a')) - 10,
811 else => unreachable,
812 };
813}
814
815fn formatIntValue(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype) !void {
816 comptime var base = 10;
817 comptime var case: Case = .lower;
818
819 const int_value = if (@TypeOf(value) == comptime_int) blk: {
820 const Int = std.math.IntFittingRange(value, value);
821 break :blk @as(Int, value);
822 } else value;
823
824 if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "d")) {
825 base = 10;
826 case = .lower;
827 } else if (comptime std.mem.eql(u8, fmt, "c")) {
828 if (@typeInfo(@TypeOf(int_value)).int.bits <= 8) {
829 return formatAsciiChar(@as(u8, int_value), options, writer);
830 } else {
831 @compileError("cannot print integer that is larger than 8 bits as an ASCII character");
832 }
833 } else if (comptime std.mem.eql(u8, fmt, "u")) {
834 if (@typeInfo(@TypeOf(int_value)).int.bits <= 21) {
835 return formatUnicodeCodepoint(@as(u21, int_value), options, writer);
836 } else {
837 @compileError("cannot print integer that is larger than 21 bits as an UTF-8 sequence");
838 }
839 } else if (comptime std.mem.eql(u8, fmt, "b")) {
840 base = 2;
841 case = .lower;
842 } else if (comptime std.mem.eql(u8, fmt, "x")) {
843 base = 16;
844 case = .lower;
845 } else if (comptime std.mem.eql(u8, fmt, "X")) {
846 base = 16;
847 case = .upper;
848 } else if (comptime std.mem.eql(u8, fmt, "o")) {
849 base = 8;
850 case = .lower;
851 } else {
852 invalidFmtError(fmt, value);
853 }
854
855 return formatInt(int_value, base, case, options, writer);
856}
857
858fn formatAsciiChar(c: u8, options: FormatOptions, writer: anytype) !void {
859 return formatBuf(@as(*const [1]u8, &c), options, writer);
860}
861
862fn formatUnicodeCodepoint(c: u21, options: FormatOptions, writer: anytype) !void {
863 var buf: [4]u8 = undefined;
864 const len = std.unicode.utf8Encode(c, &buf) catch |err| switch (err) {
865 error.Utf8CannotEncodeSurrogateHalf, error.CodepointTooLarge => {
866 return formatBuf(&std.unicode.utf8EncodeComptime(std.unicode.replacement_character), options, writer);
867 },
868 };
869 return formatBuf(buf[0..len], options, writer);
870}
871
872fn formatFloatValue(value: anytype, comptime fmt: []const u8, options: FormatOptions, writer: anytype) !void {
873 const v = switch (@TypeOf(value)) {
874 comptime_float => @as(f128, value), // comptime_float internally is a f128; this preserves precision.
875 else => value,
876 };
877 const T = @TypeOf(v);
878 comptime std.debug.assert(@typeInfo(T) == .float);
879 const I = @Int(.unsigned, @bitSizeOf(T));
880 const DT = if (@bitSizeOf(T) <= 64) u64 else u128;
881 const tables = switch (DT) {
882 u64 => &std.fmt.float.Backend64_TablesFull,
883 u128 => &std.fmt.float.Backend128_Tables,
884 else => unreachable,
885 };
886 const has_explicit_leading_bit = std.math.floatMantissaBits(T) - std.math.floatFractionalBits(T) != 0;
887 const d = std.fmt.float.binaryToDecimal(DT, @as(I, @bitCast(v)), std.math.floatMantissaBits(T), std.math.floatExponentBits(T), has_explicit_leading_bit, tables);
888 var buf: [std.fmt.float.bufferSize(.decimal, T)]u8 = undefined;
889
890 if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "e")) {
891 try writer.writeAll(std.fmt.float.formatScientific(DT, &buf, d, options.precision) catch unreachable);
892 } else if (comptime std.mem.eql(u8, fmt, "d")) {
893 try writer.writeAll(std.fmt.float.formatDecimal(DT, &buf, d, options.precision) catch unreachable);
894 } else if (comptime std.mem.eql(u8, fmt, "x")) {
895 var w: std.Io.Writer = .fixed(&buf);
896 w.printFloatHex(v, .lower, options.precision) catch unreachable;
897 try writer.writeAll(buf[0..w.end]);
898 } else {
899 invalidFmtError(fmt, value);
900 }
901}
902
903pub fn allocPrint(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) ![]u8 {
904 var aw: nio.AllocatingWriter = .init(allocator);
905 errdefer aw.deinit();
906 try aw.ensureUnusedCapacity(fmt.len);
907 try format(&aw, fmt, args);
908 return aw.toOwnedSlice();
909}
910pub fn allocPrintZ(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) ![:0]u8 {
911 const result = try allocPrint(allocator, fmt ++ "\x00", args);
912 return result[0 .. result.len - 1 :0];
913}