Skip to content

Commit

Permalink
Add human readable formatting for numbers
Browse files Browse the repository at this point in the history
* `Number#format`: Similar to `#to_s` but with more options for customizing the
  output format. Decimal separator and thousands delimiter can be specified
  (useful for localization) as well as the number of printed decimal places.
* `Number#humanize`: Pretty prints the number in a human-readable format.
  Formats the most significant digits of the number with a prefix indicating the
  magnitude. Output parameters, prefixes etc. can be fully customized.
* `Int#humanize_bytes`: Specialization of `Number#humanize` for binary units.

```cr
123.4567.format(decimal_places: 3) # => "123.457"
1_200_000_000.humanize # => "1.2G"
1536.humanize_bytes # => "1.5KiB"
```
  • Loading branch information
straight-shoota committed Feb 7, 2019
1 parent 0da24ec commit 778579e
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 46 deletions.
6 changes: 3 additions & 3 deletions spec/std/benchmark_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ private def h_mean(mean)
end

describe Benchmark::IPS::Entry, "#human_mean" do
it { h_mean(0.01234567890123).should eq(" 0.01 ") }
it { h_mean(0.12345678901234).should eq(" 0.12 ") }
it { h_mean(0.01234567890123).should eq(" 12.35m") }
it { h_mean(0.12345678901234).should eq("123.46m") }

it { h_mean(1.23456789012345).should eq(" 1.23 ") }
it { h_mean(12.3456789012345).should eq(" 12.35 ") }
Expand All @@ -94,7 +94,7 @@ private def h_ips(seconds)
end

describe Benchmark::IPS::Entry, "#human_iteration_time" do
it { h_ips(1234.567_890_123).should eq("1234.57s ") }
it { h_ips(1234.567_890_123).should eq("1,234.57s ") }
it { h_ips(123.456_789_012_3).should eq("123.46s ") }
it { h_ips(12.345_678_901_23).should eq(" 12.35s ") }
it { h_ips(1.234_567_890_123).should eq(" 1.23s ") }
Expand Down
5 changes: 5 additions & 0 deletions spec/std/big/big_int_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ describe "BigInt" do
x = 1.to_big_i
x.clone.should eq(x)
end

describe "#humanize_bytes" do
it { BigInt.new("1180591620717411303424").humanize_bytes.should eq("1.0ZiB") }
it { BigInt.new("1208925819614629174706176").humanize_bytes.should eq("1.0YiB") }
end
end

describe "BigInt Math" do
Expand Down
2 changes: 1 addition & 1 deletion spec/std/http/server/handlers/log_handler_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe HTTP::LogHandler do
handler = HTTP::LogHandler.new(log_io)
handler.next = ->(ctx : HTTP::Server::Context) { called = true }
handler.call(context)
(log_io.to_s =~ %r(GET / - 200 \(\d.+\))).should be_truthy
log_io.to_s.should match %r(GET / - 200 \(\d+(\.\d+)?[mµn]s\))
called.should be_true
end

Expand Down
111 changes: 111 additions & 0 deletions spec/std/humanize_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
require "spec"

private LENGTH_UNITS = ->(magnitude : Int32, number : Float64) do
case magnitude
when -2, -1 then {-2, " cm"}
when .>=(4)
{3, " km"}
else
magnitude = Number.prefix_index(magnitude)
{magnitude, " #{Number.si_prefix(magnitude)}m"}
end
end

describe Number do
describe "#format" do
it do
1.format.should eq "1"
12.format.should eq "12"
123.format.should eq "123"
1234.format.should eq "1,234"

123.45.format.should eq "123.45"
123.45.format(separator: ',').should eq "123,45"
123.45.format(decimal_places: 3).should eq "123.450"
123.45.format(decimal_places: 3, only_significant: true).should eq "123.45"
123.4567.format(decimal_places: 3).should eq "123.457"

123_456.format.should eq "123,456"
123_456.format(delimiter: '.').should eq "123.456"

123_456.789.format.should eq "123,456.789"
end
end

describe "#humanize" do
it { 0.humanize.should eq "0.0" }
it { 1.humanize.should eq "1.0" }
it { -1.humanize.should eq "-1.0" }
it { 123.humanize.should eq "123" }
it { 123.humanize(2).should eq "120" }
it { 1234.humanize.should eq "1.23k" }
it { 12_345.humanize.should eq "12.3k" }
it { 12_345.humanize(2).should eq "12k" }
it { 1_234_567.humanize.should eq "1.23M" }
it { 1_234_567.humanize(5).should eq "1.2346M" }
it { 12_345_678.humanize(5).should eq "12.346M" }
it { 0.012_345.humanize.should eq "12.3m" }
it { 0.001_234_5.humanize.should eq "1.23m" }
it { 0.000_000_012_345.humanize.should eq "12.3n" }
it { 0.000_000_001.humanize.should eq "1.0n" }
it { 0.000_000_001_235.humanize.should eq "1.24n" }
it { 0.123_456_78.humanize.should eq "123m" }
it { 0.123_456_78.humanize(5).should eq "123.46m" }

it { 1_234.567_890_123.humanize(precision: 2, significant: false).should eq("1.23k") }
it { 123.456_789_012_3.humanize(precision: 2, significant: false).should eq("123.46") }
it { 12.345_678_901_23.humanize(precision: 2, significant: false).should eq("12.35") }
it { 1.234_567_890_123.humanize(precision: 2, significant: false).should eq("1.23") }

it { 0.123_456_789_012.humanize(precision: 2, significant: false).should eq("123.46m") }
it { 0.012_345_678_901.humanize(precision: 2, significant: false).should eq("12.35m") }
it { 0.001_234_567_890.humanize(precision: 2, significant: false).should eq("1.23m") }

it { 0.000_123_456_789.humanize(precision: 2, significant: false).should eq("123.46µ") }
it { 0.000_012_345_678.humanize(precision: 2, significant: false).should eq("12.35µ") }
it { 0.000_001_234_567.humanize(precision: 2, significant: false).should eq("1.23µ") }

it { 0.000_000_123_456.humanize(precision: 2, significant: false).should eq("123.46n") }
it { 0.000_000_012_345.humanize(precision: 2, significant: false).should eq("12.35n") }
it { 0.000_000_001_234.humanize(precision: 2, significant: false).should eq("1.23n") }
it { 0.000_000_000_123.humanize(precision: 2, significant: false).should eq("123.00p") }

describe "using custom prefixes" do
it { 1_420_000_000.humanize(prefixes: LENGTH_UNITS).should eq "1,420,000 km" }
it { 1_420.humanize(prefixes: LENGTH_UNITS).should eq "1.42 km" }
it { 1.humanize(prefixes: LENGTH_UNITS).should eq "1.0 m" }
it { 0.1.humanize(prefixes: LENGTH_UNITS).should eq "10.0 cm" }
it { 0.01.humanize(prefixes: LENGTH_UNITS).should eq "1.0 cm" }
it { 0.001.humanize(prefixes: LENGTH_UNITS).should eq "1.0 mm" }
it { 0.000_01.humanize(prefixes: LENGTH_UNITS).should eq "10.0 µm" }
it { 0.000_000_001.humanize(prefixes: LENGTH_UNITS).should eq "1.0 nm" }
end
end
end

describe Int do
describe "#humanize_bytes" do
# default IEC
it { 1024.humanize_bytes.should eq "1.0kiB" }

it { 0.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "0B" }
it { 1.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1B" }
it { 1000.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1000B" }
it { 1014.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "0.99KB" }
it { 1015.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.0KB" }
it { 1024.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.0KB" }
it { 1025.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.01KB" }
it { 2048.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "2.0KB" }

it { 1536.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.5KB") }
it { 524288.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("512KB") }
it { 1048576.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.0MB") }
it { 1073741824.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.0GB") }
it { 1099511627776.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.0TB") }
it { 1125899906842624.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.0PB") }
it { 1152921504606846976.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.0EB") }

it { 1024.humanize_bytes(format: Int::BinaryPrefixFormat::IEC).should eq "1.0kiB" }
it { 1073741824.humanize_bytes(format: Int::BinaryPrefixFormat::IEC).should eq "1.0GiB" }
end
end
43 changes: 8 additions & 35 deletions src/benchmark/ips.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ module Benchmark
def report
max_label = ran_items.max_of &.label.size
max_compare = ran_items.max_of &.human_compare.size
max_bytes_per_op = ran_items.max_of &.bytes_per_op.to_s.size
max_bytes_per_op = ran_items.max_of &.bytes_per_op.humanize(base: 1024).size

ran_items.each do |item|
printf "%s %s (%s) (±%5.2f%%) %s B/op %s\n",
printf "%s %s (%s) (±%5.2f%%) %sB/op %s\n",
item.label.rjust(max_label),
item.human_mean,
item.human_iteration_time,
item.relative_stddev,
item.bytes_per_op.to_s.rjust(max_bytes_per_op),
item.bytes_per_op.humanize(base: 1024).rjust(max_bytes_per_op),
item.human_compare.rjust(max_compare)
end
end
Expand Down Expand Up @@ -185,43 +185,16 @@ module Benchmark
end

def human_mean
case Math.log10(mean)
when Float64::MIN..3
digits = mean
suffix = ' '
when 3..6
digits = mean / 1000
suffix = 'k'
when 6..9
digits = mean / 1_000_000
suffix = 'M'
else
digits = mean / 1_000_000_000
suffix = 'G'
end

"#{digits.round(2).to_s.rjust(6)}#{suffix}"
mean.humanize(precision: 2, significant: false, prefixes: Number::SI_PREFIXES_PADDED).rjust(7)
end

def human_iteration_time
iteration_time = 1.0 / mean

case Math.log10(iteration_time)
when 0..Float64::MAX
digits = iteration_time
suffix = "s "
when -3..0
digits = iteration_time * 1000
suffix = "ms"
when -6..-3
digits = iteration_time * 1_000_000
suffix = "µs"
else
digits = iteration_time * 1_000_000_000
suffix = "ns"
end

"#{digits.round(2).to_s.rjust(6)}#{suffix}"
iteration_time.humanize(precision: 2, significant: false) do |magnitude, _|
magnitude = Number.prefix_index(magnitude).clamp(-9..0)
{magnitude, magnitude == 0 ? "s " : "#{Number.si_prefix(magnitude)}s"}
end.rjust(8)
end

def human_compare
Expand Down
8 changes: 1 addition & 7 deletions src/http/server/handlers/log_handler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ class HTTP::LogHandler
minutes = elapsed.total_minutes
return "#{minutes.round(2)}m" if minutes >= 1

seconds = elapsed.total_seconds
return "#{seconds.round(2)}s" if seconds >= 1

millis = elapsed.total_milliseconds
return "#{millis.round(2)}ms" if millis >= 1

"#{(millis * 1000).round(2)}µs"
"#{elapsed.total_seconds.humanize(precision: 2, significant: false)}s"
end
end
Loading

0 comments on commit 778579e

Please sign in to comment.