humane 0.6.0 → 0.9.3

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: b0e7b2d5c4485d84a102a56c7586e7a61e62312729b1978d8221a24cc7d23917
4
+ data.tar.gz: c51ce6b56f73e1d859c9cb65e4a096cee6030c37fe4664c30ab16da13193d9a3
5
5
  SHA512:
6
- metadata.gz: fa9e176a20d540c3c325c6f1015c2353b2fbced72397d9bcb645eea021129341781543399ac0a94d1cde5b885e64738e7d723d14c2bfb8548c3b7f7d392664cd
7
- data.tar.gz: e59a7270df069333babc2d10f6234bbb507853b9b2c1583e437e41f1451a1f0dd6bc82cba1f88ec83989bb31a2957132f8d8c38a4bd96a056fb1a1b293ebd39f
6
+ metadata.gz: d3477e12b897535a507f1cd95c3b2c57619e71bce6f3d320c89eafde5621a5fb382f18eb9fa10a570d9edc511d2b62fe0c6ba6e65338432730ef5a1baf64b2ed
7
+ data.tar.gz: 8be3d6ed6747f5adcc66edc8eca5dc2ce7ea3fd4252af19a1e376787333a6af6a1d039590b49c885ecbe3594eb351364be7aa0e734b805bf4ad795fe2c9f47cf
data/README.md CHANGED
@@ -5,47 +5,25 @@
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"
18
+ Humane.human_size(225_935) # "226 KB"
20
19
 
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:
20
+ mtime = Time.now - 180
21
+ Humane.time_ago(mtime) # "3 minutes ago" -- relative to the real clock
27
22
 
28
- ```ruby
29
- Humane::SizeFormatter.new.string(225_935) # "226 KB"
30
- time_formatter.string(Time.now - 180, Time.now) # "3 minutes ago"
23
+ now = Time.now
24
+ Humane.distance_in_time(mtime, now) # "3 minutes ago" -- explicit relative_to, for tests
31
25
  ```
32
26
 
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"
42
- ```
43
-
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
27
  ## Install
50
28
 
51
29
  ```
@@ -58,33 +36,52 @@ or in a `Gemfile`:
58
36
  gem "humane"
59
37
  ```
60
38
 
61
- ## Beyond Foundation's defaults
39
+ ## `distance_in_time` and `time_ago`
62
40
 
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:
41
+ Two entry points, same naming split as ActionView's own
42
+ `distance_of_time_in_words`/`time_ago_in_words`:
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
+ - **`distance_in_time(at, relative_to, **opts)`** -- the explicit,
45
+ fully-tested core. Takes both times, so it's what specs should call.
46
+ - **`time_ago(at, **opts)`** -- a one-argument convenience for the common
47
+ "drop into a view" case. Supplies `Time.now` as `relative_to` internally;
48
+ everything else is identical to `distance_in_time`.
49
+
50
+ Both share the same keyword options and recommended defaults, already
51
+ matching ActionView's own `distance_of_time_in_words` defaults -- pass none
52
+ at all and you get them for free:
53
+
54
+ ```ruby
55
+ Humane.distance_in_time(at, relative_to) # approximate: true, include_seconds: false
56
+ Humane.time_ago(at) # same defaults, relative_to is Time.now
57
+ ```
58
+
59
+ - **`approximate`** (default `true`): prefixes `"about"`/`"in about"` on the
60
+ hour-scale buckets (1 hour, and 2..24 hours), matching ActionView's
61
+ `distance_of_time_in_words` wording for those buckets exactly (down to its
62
+ 44:30/89:30 rounding cutoffs), through the "1 day" bucket.
63
+ - **`include_seconds`** (default `false`): under 30 seconds, collapses to
64
+ `"less than a minute ago"`/`"in less than a minute"` instead of an exact
65
+ second count. Matches ActionView's `include_seconds` default.
66
+ - **`when_nil`** (default `nil`): if `at` is `nil`, both methods return this
67
+ value without formatting -- for a scan, download, or other record that
68
+ doesn't have a timestamp yet.
78
69
 
79
70
  ```ruby
80
- Humane::TimeFormatter.new(approximate: true).string(at: t - 15 * 3600, relative_to: t)
81
- # => "about 15 hours ago"
71
+ Humane.distance_in_time(t, now, approximate: false) # "15 hours ago", not "about 15 hours ago"
72
+ Humane.time_ago(nil, when_nil: "an unknown time") # "an unknown time"
82
73
  ```
83
74
 
84
75
  ## Scope
85
76
 
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.
77
+ Finder's byte-count style, and a numeric (non-calendar-aware) relative time
78
+ style through the "1 day" bucket -- that's the whole surface area today.
79
+ Alternate size units/styles and a `:named` style (`"yesterday"`,
80
+ calendar-boundary-aware) aren't implemented -- contributions welcome.
81
+
82
+ ## Development
83
+
84
+ ```
85
+ bundle exec standardrb
86
+ bundle exec rspec -fd spec
87
+ ```
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Humane
4
+ UNITS = %w[KB MB GB TB PB EB].freeze
5
+
6
+ # Returns byte_count as a Finder-style human-readable string.
7
+ #
8
+ # Humane.human_size(225_935) #=> "226 KB"
9
+ def self.human_size(byte_count)
10
+ return "Zero KB" if byte_count.zero?
11
+ return (byte_count == 1) ? "1 byte" : "#{byte_count} bytes" if byte_count < 1000
12
+
13
+ exponent = [(Math.log(byte_count) / Math.log(1000)).to_i, UNITS.size].min
14
+ value = byte_count / (1000.0**exponent)
15
+
16
+ "#{format_significant(value, 3)} #{UNITS[exponent - 1]}"
17
+ end
18
+
19
+ # Rounds value to sig_figs significant figures and trims trailing
20
+ # fractional zeros (and the decimal point itself, if nothing remains
21
+ # after it) -- see docs/COMMENTS.md.
22
+ def self.format_significant(value, sig_figs)
23
+ magnitude = Math.log10(value).floor + 1
24
+ decimals = [sig_figs - magnitude, 0].max
25
+
26
+ formatted = format("%.#{decimals}f", value)
27
+ formatted.include?(".") ? formatted.sub(/0+\z/, "").sub(/\.\z/, "") : formatted
28
+ end
29
+ private_class_method :format_significant
30
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Humane
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. This is
8
+ # the explicit, fully-tested core -- see .time_ago below for the
9
+ # one-argument convenience. See docs/COMMENTS.md.
10
+ #
11
+ # Humane.distance_in_time(Time.now - 180, Time.now) #=> "3 minutes ago"
12
+ def self.distance_in_time(at, relative_to, approximate: true, include_seconds: false, when_nil: nil)
13
+ return when_nil if at.nil?
14
+
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
26
+
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]
38
+ end
39
+
40
+ text = "about #{text}" if approximate && approximable
41
+ wrap(text, future: future)
42
+ end
43
+
44
+ # Returns at relative to the current time -- a convenience for the common
45
+ # "drop into a view" case, modeled on ActionView's time_ago_in_words
46
+ # wrapping distance_of_time_in_words with Time.now. Use .distance_in_time
47
+ # directly when the reference time needs to be explicit (tests, a fixed
48
+ # point other than now).
49
+ #
50
+ # Humane.time_ago(Time.now - 180) #=> "3 minutes ago"
51
+ def self.time_ago(at, **opts)
52
+ distance_in_time(at, Time.now, **opts)
53
+ end
54
+
55
+ def self.wrap(text, future:)
56
+ future ? "in #{text}" : "#{text} ago"
57
+ end
58
+ private_class_method :wrap
59
+
60
+ def self.pluralize(count, unit)
61
+ (count == 1) ? "1 #{unit}" : "#{count} #{unit}s"
62
+ end
63
+ private_class_method :pluralize
64
+ 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.3"
5
5
  end
data/lib/humane.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "humane/version"
4
- require_relative "humane/size_formatter"
5
- require_relative "humane/time_formatter"
4
+ require_relative "humane/size"
5
+ require_relative "humane/time"
6
6
 
7
7
  module Humane
8
8
  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.3
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: []
@@ -20,8 +20,8 @@ files:
20
20
  - LICENSE
21
21
  - README.md
22
22
  - lib/humane.rb
23
- - lib/humane/size_formatter.rb
24
- - lib/humane/time_formatter.rb
23
+ - lib/humane/size.rb
24
+ - lib/humane/time.rb
25
25
  - lib/humane/version.rb
26
26
  homepage: https://github.com/woodie/humane-ruby
27
27
  licenses:
@@ -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: []
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Humane
4
- # Formats byte counts the way Finder does: 1000-based math, capitalized unit labels.
5
- class SizeFormatter
6
- UNITS = %w[B KB MB GB TB PB EB].freeze
7
-
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
12
-
13
- return "#{from_byte_count} B" if from_byte_count < 1000
14
-
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
17
-
18
- if rounded < 10
19
- format("%.1f %s", rounded, UNITS[exponent])
20
- else
21
- format("%.0f %s", rounded, UNITS[exponent])
22
- end
23
- end
24
- end
25
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Humane
4
- # Formats one time relative to another the way Finder-adjacent tools do.
5
- 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
31
-
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]
43
- end
44
-
45
- text = "about #{text}" if @approximate && approximable
46
- wrap(text, future: future)
47
- end
48
-
49
- private
50
-
51
- def wrap(text, future:)
52
- future ? "in #{text}" : "#{text} ago"
53
- end
54
-
55
- def pluralize(count, unit)
56
- count == 1 ? "1 #{unit}" : "#{count} #{unit}s"
57
- end
58
- end
59
- end