Skip to content

Commit 589ca57

Browse files
committed
Finish robust float printing implementation
1 parent 96ebd8b commit 589ca57

File tree

1 file changed

+142
-27
lines changed

1 file changed

+142
-27
lines changed

std/fmt/index.zig

Lines changed: 142 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ pub fn format(context: var, comptime Errors: type, output: fn(@typeOf(context),
134134
},
135135
State.Float => switch (c) {
136136
'}' => {
137-
try formatFloatDecimal(args[next_arg], 0, context, Errors, output);
137+
try formatFloatDecimal(args[next_arg], null, context, Errors, output);
138138
next_arg += 1;
139139
state = State.Start;
140140
start_index = i + 1;
@@ -261,14 +261,14 @@ pub fn formatFloat(value: var, context: var, comptime Errors: type, output: fn(@
261261

262262
// Errol doesn't handle these special cases.
263263
if (math.isNan(x)) {
264-
return output(context, "NaN");
264+
return output(context, "nan");
265265
}
266266
if (math.signbit(x)) {
267267
try output(context, "-");
268268
x = -x;
269269
}
270270
if (math.isPositiveInf(x)) {
271-
return output(context, "Infinity");
271+
return output(context, "inf");
272272
}
273273
if (x == 0.0) {
274274
return output(context, "0.0");
@@ -294,43 +294,117 @@ pub fn formatFloat(value: var, context: var, comptime Errors: type, output: fn(@
294294
}
295295
}
296296

297-
pub fn formatFloatDecimal(value: var, precision: usize, context: var, comptime Errors: type, output: fn(@typeOf(context), []const u8)Errors!void) Errors!void {
297+
// Print a float of the format x.yyyyy where the number of y is specified by the precision argument.
298+
// The default precision is 5, values are rounded to the nearest value according to the precision.
299+
pub fn formatFloatDecimal(value: var, prec: ?usize, context: var, comptime Errors: type, output: fn(@typeOf(context), []const u8)Errors!void) Errors!void {
298300
var x = f64(value);
301+
const precision = prec ?? 5;
299302

300303
// Errol doesn't handle these special cases.
301304
if (math.isNan(x)) {
302-
return output(context, "NaN");
305+
return output(context, "nan");
303306
}
307+
304308
if (math.signbit(x)) {
305309
try output(context, "-");
306310
x = -x;
307311
}
312+
308313
if (math.isPositiveInf(x)) {
309-
return output(context, "Infinity");
314+
return output(context, "inf");
310315
}
316+
311317
if (x == 0.0) {
312-
return output(context, "0.0");
318+
try output(context, "0.");
319+
320+
var i: usize = 0;
321+
while (i < precision) : (i += 1) {
322+
try output(context, "0");
323+
}
324+
return;
313325
}
314326

327+
// non-special case, use errol3
315328
var buffer: [32]u8 = undefined;
316-
const float_decimal = errol3(x, buffer[0..]);
329+
var float_decimal = errol3(x, buffer[0..]);
330+
331+
// Find the digit which will signify the round point and start rounding backwards.
332+
const round_digit = if (float_decimal.exp >= 0) precision + usize(float_decimal.exp) else precision;
333+
if (round_digit < float_decimal.digits.len and float_decimal.digits[round_digit] - '0' >= 5) {
334+
debug.assert(round_digit >= 0);
335+
336+
var i = round_digit;
337+
while (true) {
338+
if (i == 0) {
339+
// Rounded all the way past the start. This was of the form 9.999...
340+
// If the exponent is positive we have an extra digit and handle by printing
341+
// immediately to avoid reshuffling the buffer.
342+
// If negative, increase exp to adjust the zero-padding.
343+
if (float_decimal.exp > 0) {
344+
try output(context , "1");
345+
} else {
346+
float_decimal.exp += 1;
347+
}
348+
break;
349+
}
350+
351+
i -= 1;
352+
353+
const new_value = (float_decimal.digits[i] - '0' + 1) % 10;
354+
float_decimal.digits[i] = new_value + '0';
355+
356+
// must continue rounding until non-9
357+
if (new_value != 0) {
358+
break;
359+
}
360+
}
361+
}
317362

318-
const num_left_digits = if (float_decimal.exp > 0) usize(float_decimal.exp) else 1;
363+
// exp < 0 means the leading is always 0 as errol result is normalized.
364+
const num_digits_whole = if (float_decimal.exp >= 0) usize(float_decimal.exp) else 0;
365+
if (num_digits_whole > 0) {
366+
try output(context, float_decimal.digits[0 .. num_digits_whole]);
367+
} else {
368+
try output(context , "0");
369+
}
370+
371+
// {.0} special case doesn't want a trailing '.'
372+
if (precision == 0) {
373+
return;
374+
}
319375

320-
try output(context, float_decimal.digits[0 .. num_left_digits]);
321376
try output(context, ".");
322-
if (float_decimal.digits.len > 1) {
323-
const num_valid_digtis = if (@typeOf(value) == f32) math.min(usize(7), float_decimal.digits.len)
324-
else
325-
float_decimal.digits.len;
326377

327-
const num_right_digits = if (precision != 0)
328-
math.min(precision, (num_valid_digtis-num_left_digits))
329-
else
330-
num_valid_digtis - num_left_digits;
331-
try output(context, float_decimal.digits[num_left_digits .. (num_left_digits + num_right_digits)]);
378+
// Keep track of fractional count printed for case where we pre-pad then post-pad with 0's.
379+
var printed: usize = 0;
380+
381+
// Zero-fill until we reach significant digits or run out of precision.
382+
if (float_decimal.exp < 0) {
383+
const zero_digit_count = usize(-float_decimal.exp);
384+
385+
var i: usize = 0;
386+
while (i < zero_digit_count and i < precision) : (i += 1) {
387+
try output(context, "0");
388+
printed += 1;
389+
}
390+
391+
if (i >= precision) {
392+
return;
393+
}
394+
}
395+
396+
// Remaining fractional portion, zero-padding till desired precision.
397+
const remaining_digits = float_decimal.digits.len - num_digits_whole;
398+
if (precision < remaining_digits) {
399+
try output(context, float_decimal.digits[num_digits_whole .. num_digits_whole + precision]);
400+
return;
332401
} else {
333-
try output(context, "0");
402+
try output(context, float_decimal.digits[num_digits_whole ..]);
403+
printed += float_decimal.digits.len - num_digits_whole;
404+
405+
while (printed < precision) : (printed += 1) {
406+
try output(context, "0");
407+
}
334408
}
335409
}
336410

@@ -612,17 +686,17 @@ test "fmt.format" {
612686
{
613687
var buf1: [32]u8 = undefined;
614688
const result = try bufPrint(buf1[0..], "f64: {}\n", math.nan_f64);
615-
assert(mem.eql(u8, result, "f64: NaN\n"));
689+
assert(mem.eql(u8, result, "f64: nan\n"));
616690
}
617691
{
618692
var buf1: [32]u8 = undefined;
619693
const result = try bufPrint(buf1[0..], "f64: {}\n", math.inf_f64);
620-
assert(mem.eql(u8, result, "f64: Infinity\n"));
694+
assert(mem.eql(u8, result, "f64: inf\n"));
621695
}
622696
{
623697
var buf1: [32]u8 = undefined;
624698
const result = try bufPrint(buf1[0..], "f64: {}\n", -math.inf_f64);
625-
assert(mem.eql(u8, result, "f64: -Infinity\n"));
699+
assert(mem.eql(u8, result, "f64: -inf\n"));
626700
}
627701
{
628702
var buf1: [32]u8 = undefined;
@@ -634,15 +708,15 @@ test "fmt.format" {
634708
var buf1: [32]u8 = undefined;
635709
const value: f32 = 1234.567;
636710
const result = try bufPrint(buf1[0..], "f32: {.2}\n", value);
637-
assert(mem.eql(u8, result, "f32: 1234.56\n"));
711+
assert(mem.eql(u8, result, "f32: 1234.57\n"));
638712
}
639713
{
640714
var buf1: [32]u8 = undefined;
641715
const value: f32 = -11.1234;
642716
const result = try bufPrint(buf1[0..], "f32: {.4}\n", value);
643717
// -11.1234 is converted to f64 -11.12339... internally (errol3() function takes f64).
644-
// -11.12339... is truncated to -11.1233
645-
assert(mem.eql(u8, result, "f32: -11.1233\n"));
718+
// -11.12339... is rounded back up to -11.1234
719+
assert(mem.eql(u8, result, "f32: -11.1234\n"));
646720
}
647721
{
648722
var buf1: [32]u8 = undefined;
@@ -656,7 +730,48 @@ test "fmt.format" {
656730
const result = try bufPrint(buf1[0..], "f64: {.10}\n", value);
657731
assert(mem.eql(u8, result, "f64: 91.1234567890\n"));
658732
}
659-
733+
{
734+
var buf1: [32]u8 = undefined;
735+
const value: f64 = 0.0;
736+
const result = try bufPrint(buf1[0..], "f64: {.5}\n", value);
737+
assert(mem.eql(u8, result, "f64: 0.00000\n"));
738+
}
739+
{
740+
var buf1: [32]u8 = undefined;
741+
const value: f64 = 5.700;
742+
const result = try bufPrint(buf1[0..], "f64: {.0}\n", value);
743+
assert(mem.eql(u8, result, "f64: 6\n"));
744+
}
745+
{
746+
var buf1: [32]u8 = undefined;
747+
const value: f64 = 9.999;
748+
const result = try bufPrint(buf1[0..], "f64: {.1}\n", value);
749+
assert(mem.eql(u8, result, "f64: 10.0\n"));
750+
}
751+
{
752+
var buf1: [32]u8 = undefined;
753+
const value: f64 = 1.0;
754+
const result = try bufPrint(buf1[0..], "f64: {.3}\n", value);
755+
assert(mem.eql(u8, result, "f64: 1.000\n"));
756+
}
757+
{
758+
var buf1: [32]u8 = undefined;
759+
const value: f64 = 0.0003;
760+
const result = try bufPrint(buf1[0..], "f64: {.8}\n", value);
761+
assert(mem.eql(u8, result, "f64: 0.00030000\n"));
762+
}
763+
{
764+
var buf1: [32]u8 = undefined;
765+
const value: f64 = 1.40130e-45;
766+
const result = try bufPrint(buf1[0..], "f64: {.5}\n", value);
767+
assert(mem.eql(u8, result, "f64: 0.00000\n"));
768+
}
769+
{
770+
var buf1: [32]u8 = undefined;
771+
const value: f64 = 9.999960e-40;
772+
const result = try bufPrint(buf1[0..], "f64: {.5}\n", value);
773+
assert(mem.eql(u8, result, "f64: 0.00000\n"));
774+
}
660775
}
661776
}
662777

0 commit comments

Comments
 (0)