humane 0.6.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f1d568f746432126417fdd6541c249d81475c663e4e9ae9a772fcbc1dc59e3f
4
- data.tar.gz: f85e12c6dbd402a4a1d452ba4629e6921418ff8609d6222333b17c1b51ae6a05
3
+ metadata.gz: 4d763c1ac13b418641448e26596e05164e13473257fa89c9dfbe4c752c31a780
4
+ data.tar.gz: 17672a19ade63d69bc0531d798e432b69daafe242fd9b61f594867a8bef3ee6e
5
5
  SHA512:
6
- metadata.gz: fa9e176a20d540c3c325c6f1015c2353b2fbced72397d9bcb645eea021129341781543399ac0a94d1cde5b885e64738e7d723d14c2bfb8548c3b7f7d392664cd
7
- data.tar.gz: e59a7270df069333babc2d10f6234bbb507853b9b2c1583e437e41f1451a1f0dd6bc82cba1f88ec83989bb31a2957132f8d8c38a4bd96a056fb1a1b293ebd39f
6
+ metadata.gz: a1acce1f07be40fa5aad7073b099801465adc0af544acdeb27fcacc1562249144d809c5c83130447deec443714c768c377c94334a511d232acbe43a71558012a
7
+ data.tar.gz: 617655da198133fd9d931e69ff43ae3e8cd670e56d674a42b787fb63d0f3b7aec5aabb530e22129fa5b67e2fbcc68ed7ec7d1702cea206cb7411951c9b6efb85
data/README.md CHANGED
@@ -5,47 +5,20 @@
5
5
  [![Release](https://img.shields.io/github/v/release/woodie/humane-ruby.svg)](https://github.com/woodie/humane-ruby/releases/latest)
6
6
  [![License](https://img.shields.io/github/license/woodie/humane-ruby.svg)](LICENSE)
7
7
 
8
- Getting human-readable file sizes with 1000-based math
9
- (as the Mac Finder displays) and relative times worded the way Swift's
10
- `RelativeDateTimeFormatter` does turned out to be a real challenge to get
11
- both right and simple. The `humane` library exists so a Ruby application can share
12
- consistent size and time formatting with a Swift application, instead of
13
- reaching for a library whose output doesn't match Swift's or that's
14
- complicated to drop in.
8
+ Human-readable file sizes (1000-based math, capitalized labels, the way Mac
9
+ Finder displays them) and relative times (`"3 minutes ago"`, `"in 3 hours"`)
10
+ for Ruby and Go HTML templates -- as simple to drop in as ActionView's own
11
+ helpers, with output that's consistent with
12
+ [`humane`](https://github.com/woodie/humane) (Go) and
13
+ [`humane-swift`](https://github.com/woodie/humane-swift).
15
14
 
16
15
  ```ruby
17
16
  require "humane"
18
17
 
19
- Humane::SizeFormatter.new.string(from_byte_count: 225_935) # "226 KB"
20
-
21
- time_formatter = Humane::TimeFormatter.new
22
- time_formatter.string(at: Time.now - 180, relative_to: Time.now) # "3 minutes ago"
23
- ```
24
-
25
- Both methods also accept positional arguments, matching `humane` (Go)'s
26
- positional-only calling convention:
27
-
28
- ```ruby
29
- Humane::SizeFormatter.new.string(225_935) # "226 KB"
30
- time_formatter.string(Time.now - 180, Time.now) # "3 minutes ago"
31
- ```
32
-
33
- Corresponding functions in Swift will have consistent output.
34
-
35
- ```swift
36
- import Foundation
37
-
38
- ByteCountFormatter.string(fromByteCount: Int64(225935), countStyle: .file) // "226 KB"
39
-
40
- let formatter = RelativeDateTimeFormatter(); formatter.unitsStyle = .full
41
- formatter.localizedString(for: time, relativeTo: now) // "3 minutes ago"
18
+ Humane::SizeFormatter.human_size(225_935) # "226 KB"
19
+ Humane::TimeFormatter.time_ago(Time.now - 180, Time.now) # "3 minutes ago"
42
20
  ```
43
21
 
44
- If you're writing Swift directly rather than calling Foundation by hand,
45
- [`humane-swift`](https://github.com/woodie/humane-swift) wraps these same two
46
- formatters with the identical API shape -- including the
47
- `includeSeconds`/`approximate` options below.
48
-
49
22
  ## Install
50
23
 
51
24
  ```
@@ -58,33 +31,42 @@ or in a `Gemfile`:
58
31
  gem "humane"
59
32
  ```
60
33
 
61
- ## Beyond Foundation's defaults
34
+ ## `time_ago` options
35
+
36
+ `time_ago`'s recommended defaults already match ActionView's own
37
+ `distance_of_time_in_words` defaults -- pass no keyword arguments at all and
38
+ you get them for free:
62
39
 
63
- Foundation is the baseline every default matches exactly, in all three
64
- languages -- these two options on `Humane::TimeFormatter` are how you layer
65
- ActionView's wording on top of it, not a replacement for it. Both off by
66
- default, so `Humane::TimeFormatter.new` and calling `RelativeDateTimeFormatter`
67
- directly always agree:
40
+ ```ruby
41
+ Humane::TimeFormatter.time_ago(at, relative_to) # approximate: true, include_seconds: false
42
+ ```
68
43
 
69
- - `include_seconds` (default `false`): under 30 seconds, collapses to "less than a
70
- minute ago"/"in less than a minute" instead of an exact second count. Named after
71
- ActionView's `include_seconds`, which defaults the same way.
72
- - `approximate` (default `false`): prefixes "about"/"in about" on the hour-scale
73
- buckets (1 hour, and 2..24 hours), the way ActionView's `distance_of_time_in_words`
74
- does for those same buckets -- for a render that can't refresh itself and shouldn't
75
- overstate its own precision. Matches ActionView's own table exactly (down to its
76
- 44:30/89:30 rounding cutoffs), through the "1 day" bucket; week/month/year buckets
77
- are out of scope. See [issue #1](https://github.com/woodie/humane-ruby/issues/1).
44
+ - **`approximate`** (default `true`): prefixes `"about"`/`"in about"` on the
45
+ hour-scale buckets (1 hour, and 2..24 hours), matching ActionView's
46
+ `distance_of_time_in_words` wording for those buckets exactly (down to its
47
+ 44:30/89:30 rounding cutoffs), through the "1 day" bucket.
48
+ - **`include_seconds`** (default `false`): under 30 seconds, collapses to
49
+ `"less than a minute ago"`/`"in less than a minute"` instead of an exact
50
+ second count. Matches ActionView's `include_seconds` default.
51
+ - **`when_nil`** (default `nil`): if `at` is `nil`, `time_ago` returns this
52
+ value without formatting -- for a scan, download, or other record that
53
+ doesn't have a timestamp yet.
78
54
 
79
55
  ```ruby
80
- Humane::TimeFormatter.new(approximate: true).string(at: t - 15 * 3600, relative_to: t)
81
- # => "about 15 hours ago"
56
+ Humane::TimeFormatter.time_ago(t, now, approximate: false) # "15 hours ago", not "about 15 hours ago"
57
+ Humane::TimeFormatter.time_ago(nil, now, when_nil: "an unknown time") # "an unknown time"
82
58
  ```
83
59
 
84
60
  ## Scope
85
61
 
86
- Finder's `.file` byte-count style, and a numeric (non-calendar-aware)
87
- relative time style -- that's the whole surface area today.
88
- `allowed_units`/alternate `count_style`s and a `:named` style
89
- (`"yesterday"`, calendar-boundary-aware) aren't implemented -- contributions
90
- welcome.
62
+ Finder's byte-count style, and a numeric (non-calendar-aware) relative time
63
+ style through the "1 day" bucket -- that's the whole surface area today.
64
+ Alternate size units/styles and a `:named` style (`"yesterday"`,
65
+ calendar-boundary-aware) aren't implemented -- contributions welcome.
66
+
67
+ ## Development
68
+
69
+ ```
70
+ bundle exec standardrb
71
+ bundle exec rspec -fd spec
72
+ ```
@@ -1,24 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humane
4
- # Formats byte counts the way Finder does: 1000-based math, capitalized unit labels.
4
+ # Formats byte counts the way Finder does: 1000-based math, capitalized
5
+ # unit labels, rounded to 3 significant figures. See docs/COMMENTS.md.
5
6
  class SizeFormatter
6
- UNITS = %w[B KB MB GB TB PB EB].freeze
7
+ UNITS = %w[KB MB GB TB PB EB].freeze
7
8
 
8
- # Returns from_byte_count as a Finder-style human-readable string; accepts positional or from_byte_count: keyword argument -- see docs/COMMENTS.md.
9
- def string(positional_from_byte_count = nil, from_byte_count: nil)
10
- from_byte_count ||= positional_from_byte_count
11
- raise ArgumentError, "from_byte_count is required" unless from_byte_count
9
+ class << self
10
+ # Returns byte_count as a Finder-style human-readable string.
11
+ #
12
+ # Humane::SizeFormatter.human_size(225_935) #=> "226 KB"
13
+ def human_size(byte_count)
14
+ return "Zero KB" if byte_count.zero?
15
+ return (byte_count == 1) ? "1 byte" : "#{byte_count} bytes" if byte_count < 1000
12
16
 
13
- return "#{from_byte_count} B" if from_byte_count < 1000
17
+ exponent = [(Math.log(byte_count) / Math.log(1000)).to_i, UNITS.size].min
18
+ value = byte_count / (1000.0**exponent)
14
19
 
15
- exponent = [(Math.log(from_byte_count) / Math.log(1000)).to_i, UNITS.size - 1].min
16
- rounded = (from_byte_count / (1000.0**exponent) * 10).round / 10.0
20
+ "#{format_significant(value, 3)} #{UNITS[exponent - 1]}"
21
+ end
22
+
23
+ private
24
+
25
+ # Rounds value to sig_figs significant figures and trims trailing
26
+ # fractional zeros (and the decimal point itself, if nothing remains
27
+ # after it) -- see docs/COMMENTS.md.
28
+ def format_significant(value, sig_figs)
29
+ magnitude = Math.log10(value).floor + 1
30
+ decimals = [sig_figs - magnitude, 0].max
17
31
 
18
- if rounded < 10
19
- format("%.1f %s", rounded, UNITS[exponent])
20
- else
21
- format("%.0f %s", rounded, UNITS[exponent])
32
+ formatted = format("%.#{decimals}f", value)
33
+ formatted.include?(".") ? formatted.sub(/0+\z/, "").sub(/\.\z/, "") : formatted
22
34
  end
23
35
  end
24
36
  end
@@ -1,59 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humane
4
- # Formats one time relative to another the way Finder-adjacent tools do.
4
+ # Formats one time relative to another the way ActionView's
5
+ # distance_of_time_in_words does for wording, but direction-aware like
6
+ # RelativeDateTimeFormatter -- "X ago"/"in X", chosen automatically rather
7
+ # than requiring the caller to know which applies ahead of time. See
8
+ # docs/COMMENTS.md.
5
9
  class TimeFormatter
6
- # include_seconds shows exact seconds under a minute instead of collapsing to "less than a minute ago/in less than a minute". Defaults to false, matching ActionView's include_seconds.
7
- # approximate prefixes "about"/"in about" on the hour-scale buckets (1 hour, and 2..24 hours), matching ActionView's distance_of_time_in_words wording for those buckets. Defaults to false. See docs/COMMENTS.md and humane-ruby issue #1 for the full bucket table this ports.
8
- def initialize(include_seconds: false, approximate: false)
9
- @include_seconds = include_seconds
10
- @approximate = approximate
11
- end
12
-
13
- # Returns at relative to relative_to; accepts positional or at:/relative_to: keyword arguments -- see docs/COMMENTS.md.
14
- def string(positional_at = nil, positional_relative_to = nil, at: nil, relative_to: nil)
15
- at ||= positional_at
16
- relative_to ||= positional_relative_to
17
- raise ArgumentError, "at is required" unless at
18
- raise ArgumentError, "relative_to is required" unless relative_to
19
-
20
- seconds = relative_to - at
21
- future = seconds.negative?
22
- seconds = seconds.abs
23
-
24
- if !@include_seconds && seconds < 30
25
- return future ? "in less than a minute" : "less than a minute ago"
26
- end
27
-
28
- if @include_seconds && seconds < 60
29
- return wrap(pluralize(seconds.to_i, "second"), future: future)
30
- end
10
+ class << self
11
+ # Returns at relative to relative_to as a human-readable string. If at
12
+ # is nil, returns when_nil without formatting -- see docs/COMMENTS.md.
13
+ #
14
+ # Humane::TimeFormatter.time_ago(Time.now - 180, Time.now) #=> "3 minutes ago"
15
+ def time_ago(at, relative_to, approximate: true, include_seconds: false, when_nil: nil)
16
+ return when_nil if at.nil?
17
+
18
+ seconds = relative_to - at
19
+ future = seconds.negative?
20
+ seconds = seconds.abs
21
+
22
+ if !include_seconds && seconds < 30
23
+ return future ? "in less than a minute" : "less than a minute ago"
24
+ end
31
25
 
32
- # Buckets come from distance_in_minutes, not raw seconds re-divided per unit -- see docs/COMMENTS.md.
33
- distance_in_minutes = (seconds / 60.0).round
34
-
35
- text, approximable =
36
- case distance_in_minutes
37
- when 1 then ["1 minute", false]
38
- when 2..44 then [pluralize(distance_in_minutes, "minute"), false]
39
- when 45..89 then ["1 hour", true]
40
- when 90..1439 then [pluralize((distance_in_minutes / 60.0).round, "hour"), true]
41
- when 1440..2519 then ["1 day", false]
42
- else [pluralize((distance_in_minutes / 1440.0).round, "day"), false]
26
+ if include_seconds && seconds < 60
27
+ return wrap(pluralize(seconds.to_i, "second"), future: future)
43
28
  end
44
29
 
45
- text = "about #{text}" if @approximate && approximable
46
- wrap(text, future: future)
47
- end
30
+ # Buckets come from distance_in_minutes, not raw seconds re-divided per unit -- see docs/COMMENTS.md.
31
+ distance_in_minutes = (seconds / 60.0).round
32
+
33
+ text, approximable =
34
+ case distance_in_minutes
35
+ when 1 then ["1 minute", false]
36
+ when 2..44 then [pluralize(distance_in_minutes, "minute"), false]
37
+ when 45..89 then ["1 hour", true]
38
+ when 90..1439 then [pluralize((distance_in_minutes / 60.0).round, "hour"), true]
39
+ when 1440..2519 then ["1 day", false]
40
+ else [pluralize((distance_in_minutes / 1440.0).round, "day"), false]
41
+ end
42
+
43
+ text = "about #{text}" if approximate && approximable
44
+ wrap(text, future: future)
45
+ end
48
46
 
49
- private
47
+ private
50
48
 
51
- def wrap(text, future:)
52
- future ? "in #{text}" : "#{text} ago"
53
- end
49
+ def wrap(text, future:)
50
+ future ? "in #{text}" : "#{text} ago"
51
+ end
54
52
 
55
- def pluralize(count, unit)
56
- count == 1 ? "1 #{unit}" : "#{count} #{unit}s"
53
+ def pluralize(count, unit)
54
+ (count == 1) ? "1 #{unit}" : "#{count} #{unit}s"
55
+ end
57
56
  end
58
57
  end
59
58
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humane
4
- VERSION = "0.6.0"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: humane
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Woodell
@@ -9,8 +9,8 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Finder-accurate file sizes and relative dates for Ruby, modeled on Swift's
13
- ByteCountFormatter and RelativeDateTimeFormatter.
12
+ description: Finder-style file sizes and ActionView-flavored relative dates for Ruby,
13
+ consistent with the humane (Go) and humane-swift siblings.
14
14
  email:
15
15
  - woodie@netpress.com
16
16
  executables: []
@@ -45,5 +45,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
45
  requirements: []
46
46
  rubygems_version: 3.6.9
47
47
  specification_version: 4
48
- summary: Swift's file sizes and relative dates for Ruby
48
+ summary: Finder-style file sizes and relative dates, as simple as ActionView
49
49
  test_files: []