humane 0.5.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: 94b64259f8b2dc0d78f3f6feb6b48b4d908f23bfa74f133d901c57f5232294bd
4
- data.tar.gz: 65e61cee90061fb0eba0e08053c2d24104d7e7d8bc711b8789661a5b27dd9f52
3
+ metadata.gz: 4d763c1ac13b418641448e26596e05164e13473257fa89c9dfbe4c752c31a780
4
+ data.tar.gz: 17672a19ade63d69bc0531d798e432b69daafe242fd9b61f594867a8bef3ee6e
5
5
  SHA512:
6
- metadata.gz: a3320656c10207ff6e941325d26085bc16acb4a56807cf4daafd2bb7560396cd36a37afcc77b270114b330d83ae9e0a7b8b394444958b0a7f96207a775c9492a
7
- data.tar.gz: 84d09dac947371900807718502d17055a914b91e58833a10fc2868c0179794fbf8ba80a42d14b359aef4fa4c32c2dbdcaa9265d737195f90c7978bc381a2dce2
6
+ metadata.gz: a1acce1f07be40fa5aad7073b099801465adc0af544acdeb27fcacc1562249144d809c5c83130447deec443714c768c377c94334a511d232acbe43a71558012a
7
+ data.tar.gz: 617655da198133fd9d931e69ff43ae3e8cd670e56d674a42b787fb63d0f3b7aec5aabb530e22129fa5b67e2fbcc68ed7ec7d1702cea206cb7411951c9b6efb85
data/README.md CHANGED
@@ -5,39 +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"
18
+ Humane::SizeFormatter.human_size(225_935) # "226 KB"
19
+ Humane::TimeFormatter.time_ago(Time.now - 180, Time.now) # "3 minutes ago"
23
20
  ```
24
21
 
25
- Corresponding functions in Swift will have consistent output.
26
-
27
- ```swift
28
- import Foundation
29
-
30
- ByteCountFormatter.string(fromByteCount: Int64(225935), countStyle: .file) // "226 KB"
31
-
32
- let formatter = RelativeDateTimeFormatter(); formatter.unitsStyle = .full
33
- formatter.localizedString(for: time, relativeTo: now) // "3 minutes ago"
34
- ```
35
-
36
- If you're writing Swift directly rather than calling Foundation by hand,
37
- [`humane-swift`](https://github.com/woodie/humane-swift) wraps these same two
38
- formatters with the identical API shape -- including the
39
- `includeSeconds`/`approximate` options below.
40
-
41
22
  ## Install
42
23
 
43
24
  ```
@@ -50,30 +31,42 @@ or in a `Gemfile`:
50
31
  gem "humane"
51
32
  ```
52
33
 
53
- ## Beyond Foundation's defaults
34
+ ## `time_ago` options
54
35
 
55
- Two options on `Humane::TimeFormatter`, both off by default so it matches
56
- `RelativeDateTimeFormatter` exactly out of the box:
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:
57
39
 
58
- - `include_seconds` (default `false`): under 30 seconds, collapses to "less than a
59
- minute ago"/"in less than a minute" instead of an exact second count. Named after
60
- ActionView's `include_seconds`, which defaults the same way.
61
- - `approximate` (default `false`): prefixes "about"/"in about" on the hour-scale
62
- buckets (1 hour, and 2..24 hours), the way ActionView's `distance_of_time_in_words`
63
- does for those same buckets -- for a render that can't refresh itself and shouldn't
64
- overstate its own precision. Matches ActionView's own table exactly (down to its
65
- 44:30/89:30 rounding cutoffs), through the "1 day" bucket; week/month/year buckets
66
- are out of scope. See [issue #1](https://github.com/woodie/humane-ruby/issues/1).
40
+ ```ruby
41
+ Humane::TimeFormatter.time_ago(at, relative_to) # approximate: true, include_seconds: false
42
+ ```
43
+
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.
67
54
 
68
55
  ```ruby
69
- Humane::TimeFormatter.new(approximate: true).string(at: t - 15 * 3600, relative_to: t)
70
- # => "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"
71
58
  ```
72
59
 
73
60
  ## Scope
74
61
 
75
- Finder's `.file` byte-count style, and a numeric (non-calendar-aware)
76
- relative time style -- that's the whole surface area today.
77
- `allowed_units`/alternate `count_style`s and a `:named` style
78
- (`"yesterday"`, calendar-boundary-aware) aren't implemented -- contributions
79
- 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,21 +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.
9
- def string(from_byte_count:)
10
- return "#{from_byte_count} B" if from_byte_count < 1000
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
11
16
 
12
- exponent = [(Math.log(from_byte_count) / Math.log(1000)).to_i, UNITS.size - 1].min
13
- rounded = (from_byte_count / (1000.0**exponent) * 10).round / 10.0
17
+ exponent = [(Math.log(byte_count) / Math.log(1000)).to_i, UNITS.size].min
18
+ value = byte_count / (1000.0**exponent)
14
19
 
15
- if rounded < 10
16
- format("%.1f %s", rounded, UNITS[exponent])
17
- else
18
- format("%.0f %s", rounded, UNITS[exponent])
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
31
+
32
+ formatted = format("%.#{decimals}f", value)
33
+ formatted.include?(".") ? formatted.sub(/0+\z/, "").sub(/\.\z/, "") : formatted
19
34
  end
20
35
  end
21
36
  end
@@ -1,54 +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 the time at `at` relative to `relative_to` as a human-readable string.
14
- def string(at:, relative_to:)
15
- seconds = relative_to - at
16
- future = seconds.negative?
17
- seconds = seconds.abs
18
-
19
- if !@include_seconds && seconds < 30
20
- return future ? "in less than a minute" : "less than a minute ago"
21
- end
22
-
23
- if @include_seconds && seconds < 60
24
- return wrap(pluralize(seconds.to_i, "second"), future: future)
25
- 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
26
25
 
27
- # Buckets come from distance_in_minutes, not raw seconds re-divided per unit -- see docs/COMMENTS.md.
28
- distance_in_minutes = (seconds / 60.0).round
29
-
30
- text, approximable =
31
- case distance_in_minutes
32
- when 1 then ["1 minute", false]
33
- when 2..44 then [pluralize(distance_in_minutes, "minute"), false]
34
- when 45..89 then ["1 hour", true]
35
- when 90..1439 then [pluralize((distance_in_minutes / 60.0).round, "hour"), true]
36
- when 1440..2519 then ["1 day", false]
37
- 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)
38
28
  end
39
29
 
40
- text = "about #{text}" if @approximate && approximable
41
- wrap(text, future: future)
42
- 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
43
46
 
44
- private
47
+ private
45
48
 
46
- def wrap(text, future:)
47
- future ? "in #{text}" : "#{text} ago"
48
- end
49
+ def wrap(text, future:)
50
+ future ? "in #{text}" : "#{text} ago"
51
+ end
49
52
 
50
- def pluralize(count, unit)
51
- count == 1 ? "1 #{unit}" : "#{count} #{unit}s"
53
+ def pluralize(count, unit)
54
+ (count == 1) ? "1 #{unit}" : "#{count} #{unit}s"
55
+ end
52
56
  end
53
57
  end
54
58
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humane
4
- VERSION = "0.5.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.5.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: []