smalltime 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 04123df6d8a7d7b789729fba23d69233f706654cf47d103c3d68d4bc91f32a90
4
+ data.tar.gz: 7a12888c2ba88c08948dfdd92ec403f5404823d07449c8171d98c7783b4867db
5
+ SHA512:
6
+ metadata.gz: fda736d0f883fad117a1260384695e69428003b422ed5694016050e3dc94b5b13243e3003002239928935721556483d284fc25b7abd7c849601ec501285cec34
7
+ data.tar.gz: 658d45e19966745e11d40f7da38e08e47d9c1030764c4adb4dfb646ef716de3816850bd7d45c64d4d7bf112ec54bfc2c0faa496817f0b2ab826476d134203207
@@ -0,0 +1,14 @@
1
+ module Smalltime
2
+ class Configuration
3
+ attr_accessor :maximum_unit, :strict
4
+
5
+ def initialize
6
+ @strict = false
7
+ @maximum_unit = nil
8
+ end
9
+
10
+ def strict?
11
+ @strict
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ require "smalltime"
2
+
3
+ class Numeric
4
+ Smalltime::Units.constants.each do |unit|
5
+ unit = unit.downcase
6
+ plural_unit = "#{unit}s"
7
+
8
+ define_method unit do
9
+ Smalltime::Duration.new("#{plural_unit}": self)
10
+ end
11
+
12
+ define_method plural_unit do
13
+ Smalltime::Duration.new("#{plural_unit}": self)
14
+ end
15
+ end
16
+ end
data/lib/smalltime.rb ADDED
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "smalltime/configuration"
4
+
5
+ # Smalltime is a drop-in replacement for ActiveSupport::Duration that supports arbitrary
6
+ # precision, no floating point arithmetic, and advanced formatting capabilities. It has
7
+ # no dependencies and can cooperate with ActiveSupport::Durations.
8
+ module Smalltime
9
+ class << self
10
+ attr_writer :configuration
11
+
12
+ def configuration
13
+ @configuration ||= Configuration.new
14
+ end
15
+
16
+ def reset
17
+ @configuration = Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration)
22
+ end
23
+
24
+ def units
25
+ unts = configuration.strict? ? STRICT_UNITS : NONSTRICT_UNITS
26
+
27
+ unts[0..unts.find_index do |u|
28
+ u.names.include?(Smalltime.configuration.maximum_unit)
29
+ end]
30
+ end
31
+
32
+ def unit_by_name(name)
33
+ (configuration.strict? ? STRICT_UNITS : NONSTRICT_UNITS).find do |unit|
34
+ names = [unit.name, unit.plural, unit.shortname].map { |s| s.to_s }
35
+ names += names.map { |s| s.to_sym }
36
+ names.include?(name)
37
+ end
38
+ end
39
+ end
40
+
41
+ class Unit
42
+ # Initialize a new unit with the given names, equal to the given base unit
43
+ # multiplied by the given multiplier.
44
+ def initialize(singular_name, plural_name, shortname, base: 1, multiplier: 1)
45
+ @singular_name = singular_name
46
+ @plural_name = plural_name
47
+ @shortname = shortname
48
+ @base = base
49
+ @multiplier = multiplier
50
+ end
51
+
52
+ def *(other)
53
+ if other.is_a?(Numeric)
54
+ return @base * @multiplier * other
55
+ end
56
+
57
+ (@base * @multiplier) * (other.base * other.multiplier)
58
+ end
59
+
60
+ attr_reader :multiplier, :shortname
61
+
62
+ def to_sym
63
+ @plural_name.to_sym
64
+ end
65
+
66
+ def name
67
+ @singular_name
68
+ end
69
+
70
+ def pluralize
71
+ @plural_name
72
+ end
73
+ alias_method :plural, :pluralize
74
+
75
+ def names
76
+ [
77
+ @singular_name.to_s,
78
+ @plural_name.to_s,
79
+ @shortname.to_s,
80
+ @singular_name.to_sym,
81
+ @plural_name.to_sym,
82
+ @shortname.to_sym
83
+ ]
84
+ end
85
+
86
+ def inspect
87
+ "#<Smalltime::#{@plural_name}>"
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :base
93
+ end
94
+
95
+ YOCTOSECOND = Unit.new(:yoctosecond, :yoctoseconds, :ys)
96
+ ZEPTOSECOND = Unit.new(:zeptosecond, :zeptoseconds, :zs, base: YOCTOSECOND, multiplier: 1000)
97
+ ATTOSECOND = Unit.new(:attosecond, :attoseconds, :as, base: ZEPTOSECOND, multiplier: 1000)
98
+ FEMTOSECOND = Unit.new(:femtosecond, :femtoseconds, :fs, base: ATTOSECOND, multiplier: 1000)
99
+ PICOSECOND = Unit.new(:picosecond, :picoseconds, :ps, base: FEMTOSECOND, multiplier: 1000)
100
+ NANOSECOND = Unit.new(:nanosecond, :nanoseconds, :ns, base: PICOSECOND, multiplier: 1000)
101
+ MICROSECOND = Unit.new(:microsecond, :microseconds, :µs, base: NANOSECOND, multiplier: 1000)
102
+ MILLISECOND = Unit.new(:millisecond, :milliseconds, :ms, base: MICROSECOND, multiplier: 1000)
103
+ CENTISECOND = Unit.new(:centisecond, :centiseconds, :cs, base: MILLISECOND, multiplier: 10)
104
+ DECISECOND = Unit.new(:decisecond, :deciseconds, :ds, base: CENTISECOND, multiplier: 10)
105
+
106
+ STRICT_SECOND = Unit.new(:second, :seconds, :s, base: DECISECOND, multiplier: 10)
107
+ STRICT_MINUTE = Unit.new(:minute, :minutes, :min, base: STRICT_SECOND, multiplier: 60)
108
+ STRICT_HOUR = Unit.new(:hour, :hours, :h, base: STRICT_MINUTE, multiplier: 60)
109
+ STRICT_DAY = Unit.new(:day, :days, :d, base: STRICT_HOUR, multiplier: 24)
110
+ STRICT_WEEK = Unit.new(:week, :weeks, :w, base: STRICT_DAY, multiplier: 7)
111
+ STRICT_MONTH = Unit.new(:month, :months, :mo, base: STRICT_WEEK, multiplier: 4) # Warning: approximate
112
+ STRICT_YEAR = Unit.new(:year, :years, :y, base: STRICT_MONTH, multiplier: 12)
113
+ STRICT_DECADE = Unit.new(:decade, :decades, :dy, base: STRICT_YEAR, multiplier: 10)
114
+ STRICT_CENTURY = Unit.new(:century, :centuries, :c, base: STRICT_DECADE, multiplier: 10)
115
+ STRICT_MILLENIUM = Unit.new(:millenium, :millenia, :mi, base: STRICT_CENTURY, multiplier: 10)
116
+
117
+ NONSTRICT_SECOND = Unit.new(:second, :seconds, :s, base: MILLISECOND, multiplier: 1000)
118
+ NONSTRICT_MINUTE = Unit.new(:minute, :minutes, :m, base: NONSTRICT_SECOND, multiplier: 60)
119
+ NONSTRICT_HOUR = Unit.new(:hour, :hours, :h, base: NONSTRICT_MINUTE, multiplier: 60)
120
+ NONSTRICT_DAY = Unit.new(:day, :days, :d, base: NONSTRICT_HOUR, multiplier: 24)
121
+ NONSTRICT_WEEK = Unit.new(:week, :weeks, :w, base: NONSTRICT_DAY, multiplier: 7)
122
+ NONSTRICT_MONTH = Unit.new(:month, :months, :mo, base: NONSTRICT_WEEK, multiplier: 4) # Warning: approximate
123
+ NONSTRICT_YEAR = Unit.new(:year, :years, :y, base: NONSTRICT_MONTH, multiplier: 12)
124
+ NONSTRICT_DECADE = Unit.new(:decade, :decades, :dy, base: NONSTRICT_YEAR, multiplier: 10)
125
+ NONSTRICT_CENTURY = Unit.new(:century, :centuries, :c, base: NONSTRICT_DECADE, multiplier: 10)
126
+ NONSTRICT_MILLENIUM = Unit.new(:millenium, :millenia, :mi, base: NONSTRICT_CENTURY, multiplier: 10)
127
+
128
+ STRICT_UNITS = [
129
+ YOCTOSECOND, ZEPTOSECOND, ATTOSECOND, FEMTOSECOND, PICOSECOND, NANOSECOND,
130
+ MICROSECOND, MILLISECOND, CENTISECOND, DECISECOND, STRICT_SECOND, STRICT_MINUTE,
131
+ STRICT_HOUR, STRICT_DAY, STRICT_WEEK, STRICT_MONTH, STRICT_YEAR, STRICT_DECADE,
132
+ STRICT_CENTURY, STRICT_MILLENIUM
133
+ ]
134
+ NONSTRICT_UNITS = [
135
+ YOCTOSECOND, ZEPTOSECOND, ATTOSECOND, FEMTOSECOND, PICOSECOND, NANOSECOND,
136
+ MICROSECOND, MILLISECOND, NONSTRICT_SECOND, NONSTRICT_MINUTE, NONSTRICT_HOUR,
137
+ NONSTRICT_DAY, NONSTRICT_WEEK, NONSTRICT_MONTH, NONSTRICT_YEAR, NONSTRICT_DECADE,
138
+ NONSTRICT_CENTURY, NONSTRICT_MILLENIUM
139
+ ]
140
+
141
+ # Duration represents a segment of time.
142
+ class Duration
143
+ # Create a new smalltime. A smalltime is an arbitrarily precise duration immune to
144
+ # floating point errors and having advanced formatting capabilities.
145
+ def initialize(
146
+ millenia: 0,
147
+ centuries: 0,
148
+ decades: 0,
149
+ years: 0,
150
+ months: 0,
151
+ weeks: 0,
152
+ days: 0,
153
+ hours: 0,
154
+ minutes: 0,
155
+ seconds: 0,
156
+ deciseconds: 0,
157
+ centiseconds: 0,
158
+ milliseconds: 0,
159
+ microseconds: 0,
160
+ nanoseconds: 0,
161
+ picoseconds: 0,
162
+ femtoseconds: 0,
163
+ attoseconds: 0,
164
+ zeptoseconds: 0,
165
+ yoctoseconds: 0
166
+ )
167
+ @parts = {
168
+ yoctoseconds: yoctoseconds,
169
+ zeptoseconds: zeptoseconds,
170
+ attoseconds: attoseconds,
171
+ femtoseconds: femtoseconds,
172
+ picoseconds: picoseconds,
173
+ nanoseconds: nanoseconds,
174
+ microseconds: microseconds,
175
+ milliseconds: milliseconds,
176
+ centiseconds: centiseconds,
177
+ deciseconds: deciseconds,
178
+ seconds: seconds,
179
+ minutes: minutes,
180
+ hours: hours,
181
+ days: days,
182
+ weeks: weeks,
183
+ months: months,
184
+ years: years,
185
+ decades: decades,
186
+ centuries: centuries,
187
+ millenia: millenia
188
+ }
189
+
190
+ Smalltime.units[0..-2].each.with_index do |unit, i|
191
+ next unless @parts.key?(unit.plural)
192
+
193
+ div, mod = @parts[unit.plural].divmod(Smalltime.units[i + 1].multiplier)
194
+ @parts[unit.plural] = mod
195
+ @parts[Smalltime.units[i + 1].plural] += div
196
+ end
197
+ end
198
+
199
+ def millenia
200
+ @parts[:millenia]
201
+ end
202
+
203
+ def centuries
204
+ @parts[:centuries]
205
+ end
206
+
207
+ def decades
208
+ @parts[:decades]
209
+ end
210
+
211
+ def years
212
+ @parts[:years]
213
+ end
214
+
215
+ def months
216
+ @parts[:months]
217
+ end
218
+
219
+ def weeks
220
+ @parts[:weeks]
221
+ end
222
+
223
+ def days
224
+ @parts[:days]
225
+ end
226
+
227
+ def hours
228
+ @parts[:hours]
229
+ end
230
+
231
+ def minutes
232
+ @parts[:minutes]
233
+ end
234
+
235
+ def seconds
236
+ @parts[:seconds]
237
+ end
238
+
239
+ def deciseconds
240
+ @parts[:deciseconds]
241
+ end
242
+
243
+ def centiseconds
244
+ @parts[:centiseconds]
245
+ end
246
+
247
+ def milliseconds
248
+ @parts[:milliseconds]
249
+ end
250
+
251
+ def microseconds
252
+ @parts[:microseconds]
253
+ end
254
+
255
+ def nanoseconds
256
+ @parts[:nanoseconds]
257
+ end
258
+
259
+ def picoseconds
260
+ @parts[:picoseconds]
261
+ end
262
+
263
+ def femtoseconds
264
+ @parts[:femtoseconds]
265
+ end
266
+
267
+ def attoseconds
268
+ @parts[:attoseconds]
269
+ end
270
+
271
+ def zeptoseconds
272
+ @parts[:zeptoseconds]
273
+ end
274
+
275
+ def yoctoseconds
276
+ @parts[:yoctoseconds]
277
+ end
278
+
279
+ # Given a part key like :seconds, returns the next-biggest part key like :minutes.
280
+ # If `by` is given, instead returns the part `by` levels bigger. For example,
281
+ # `bigger_part(:seconds, by: 2)` returns `:hours`.
282
+ def bigger_part(part, by: 1)
283
+ @parts.keys[@parts.keys.find_index(part) + by]
284
+ end
285
+
286
+ # Given a part key like :minutes, returns the next-smallest part key like :seconds.
287
+ # If `by` is given, instead returns the part `by` levels smaller. For example,
288
+ # `smaller_part(:hours, by: 2)` returns `:seconds`.
289
+ def smaller_part(part, by: 1)
290
+ @parts.keys[@parts.keys.find_index(part) - by]
291
+ end
292
+
293
+ def precision
294
+ @parts.each do |unit, value|
295
+ return unit if value != 0
296
+ end
297
+ end
298
+
299
+ def to_s
300
+ end
301
+
302
+ # Create a new Smalltime::Duration out of seconds. This method is provided as a drop-in replacement for
303
+ # ActiveSupport::Duration.build.
304
+ #
305
+ # @param seconds [Fixnum] the number of total seconds in the duration
306
+ # @return [Smalltime] the new smalltime duration
307
+ def build(seconds)
308
+ new(seconds: seconds)
309
+ end
310
+
311
+ # Return a duration formatted like "HH:MM:SS". If precise is true, it returns a time
312
+ # like "HH:MM:SS.cc" instead.
313
+ #
314
+ # When sign is :always, the returned value always has a "+" or "-" in front of it.
315
+ # When sign is :never, the returned value never has a sign in front of it. When sign
316
+ # is :negative, only negative values have a sign (default).
317
+ def clocklike(precise: false, sign: :negative)
318
+ format = ["%02d", ":%02d", ":%02d"]
319
+ components = [hours, minutes, seconds]
320
+
321
+ if precise
322
+ format << ".%02d"
323
+ components << (milliseconds / 10)
324
+ end
325
+
326
+ if sign == :always || (sign == :negative && negative?)
327
+ format[0] = "%0+2d"
328
+ end
329
+
330
+ Kernel.format(format.join, *components)
331
+ end
332
+
333
+ # Return a duration imprecisely formatted like "123s" or "4mo 3d". The number of
334
+ # units displayed is `units`, with the largest nonzero unit being the first and the
335
+ # remaining ones sequentially getting more precise (e.g. "3mo 21d", "4h 0m").
336
+ #
337
+ # If the duration is exactly 0, "-" is returned.
338
+ def casual(units: 1)
339
+ string_components = []
340
+
341
+ units.times do |unit_offset|
342
+ return "-" if biggest_nonzero_unit.nil?
343
+ u = Smalltime.unit_by_name(smaller_part(biggest_nonzero_unit, by: unit_offset))
344
+ string_components << "#{@parts[u.pluralize.to_sym]}#{u.shortname}"
345
+ end
346
+
347
+ return "-" if string_components.empty?
348
+ string_components.join(" ")
349
+ end
350
+
351
+ def biggest_nonzero_unit
352
+ @parts.to_a.reverse.to_h.find do |unit, val|
353
+ if val != 0
354
+ return unit
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
data/lib/smalltime.rbs ADDED
@@ -0,0 +1,53 @@
1
+ # Smalltime is a drop-in replacement for ActiveSupport::Duration that supports arbitrary
2
+ # precision, no floating point arithmetic, and advanced formatting capabilities. It has
3
+ # no dependencies and can cooperate with ActiveSupport::Durations.
4
+ module Smalltime
5
+ module Units
6
+ YOCTOSECOND: ::Integer
7
+ ZEPTOSECOND: ::Integer
8
+ ATTOSECOND: ::Integer
9
+ FEMTOSECOND: ::Integer
10
+ PICOSECOND: ::Integer
11
+ NANOSECOND: ::Integer
12
+ MICROSECOND: ::Integer
13
+ MILLISECOND: ::Integer
14
+ CENTISECOND: ::Integer
15
+ DECISECOND: ::Integer
16
+ SECOND: ::Integer
17
+ MINUTE: ::Integer
18
+ HOUR: ::Integer
19
+ DAY: ::Integer
20
+ WEEK: ::Integer
21
+ MONTH: ::Integer
22
+ YEAR: ::Integer
23
+ DECADE: ::Integer
24
+ CENTURY: ::Integer
25
+ MILLENIUM: ::Integer
26
+ end
27
+
28
+ # Duration represents a segment of time.
29
+ class Duration
30
+ # Create a new smalltime. A smalltime is an arbitrarily precise duration immune to
31
+ # floating point errors and having advanced formatting capabilities.
32
+ def initialize: (?seconds: ::Integer seconds, ?deciseconds: ::Integer deciseconds, ?centiseconds: ::Integer centiseconds, ?milliseconds: ::Integer milliseconds, ?microseconds: ::Integer microseconds, ?nanoseconds: ::Integer nanoseconds, ?picoseconds: ::Integer picoseconds, ?femtoseconds: ::Integer femtoseconds, ?attoseconds: ::Integer attoseconds, ?zeptoseconds: ::Integer zeptoseconds, ?yoctoseconds: ::Integer yoctoseconds) -> void
33
+
34
+ def precision: () -> untyped
35
+
36
+ def to_s: () -> nil
37
+
38
+ # Create a new Smalltime::Duration out of seconds. This method is provided as a drop-in replacement for
39
+ # ActiveSupport::Duration.build.
40
+ #
41
+ # @param seconds [Fixnum] the number of total seconds in the duration
42
+ # @return [Smalltime] the new smalltime duration
43
+ def build: (untyped seconds) -> untyped
44
+
45
+ # format returns a time like "HH:MM:SS". If precise is true, it returns a time like
46
+ # "HH:MM:SS.cc" instead.
47
+ #
48
+ # When sign is :always, the returned value always has a "+" or "-" in front of it.
49
+ # When sign is :never, the returned value never has a sign in front of it. When sign
50
+ # is :negatives, only negative values have a sign (default).
51
+ def format: (?precise: bool precise, ?sign: ::Symbol sign) -> ("-" | untyped)
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smalltime
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Carlsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ Smalltime is a Ruby duration library that focuses on providing a great experience
15
+ when working with small durations like hours, minutes, seconds, milliseconds,
16
+ nanoseconds, or smaller (down to yoctoseconds).
17
+
18
+ Smalltime does not perform any floating point calculations, so there is no risk of
19
+ floating point errors.
20
+ email: ben@twos.dev
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - lib/smalltime.rb
26
+ - lib/smalltime.rbs
27
+ - lib/smalltime/configuration.rb
28
+ - lib/smalltime/numeric.rb
29
+ homepage: https://rubygems.org/gems/smalltime
30
+ licenses:
31
+ - MIT
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.1.2
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Subsecond Ruby durations without floating point arithmetic
52
+ test_files: []