timeprice 0.1.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +63 -0
- data/README.md +98 -4
- data/lib/timeprice/cli/formatting.rb +34 -0
- data/lib/timeprice/cli/presenters/compare.rb +44 -0
- data/lib/timeprice/cli/presenters/exchange.rb +45 -0
- data/lib/timeprice/cli/presenters/inflation.rb +36 -0
- data/lib/timeprice/cli/presenters/sources.rb +65 -0
- data/lib/timeprice/cli.rb +83 -118
- data/lib/timeprice/compare.rb +30 -40
- data/lib/timeprice/cpi_lookup.rb +62 -0
- data/lib/timeprice/data_loader.rb +31 -5
- data/lib/timeprice/errors.rb +12 -5
- data/lib/timeprice/exchange.rb +15 -3
- data/lib/timeprice/inflation.rb +31 -55
- data/lib/timeprice/point.rb +62 -0
- data/lib/timeprice/sources/coverage.rb +71 -0
- data/lib/timeprice/sources.rb +3 -49
- data/lib/timeprice/supported.rb +62 -0
- data/lib/timeprice/version.rb +1 -1
- data/lib/timeprice.rb +40 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b400f9dc1652d4476d884f59a2721e4f07338e76c5cc7afab50896267e1123c
|
|
4
|
+
data.tar.gz: 1a894e2464aa1f1495ddaa24c284b819a8afc07309cd3080f7c8c70744959ef2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f2c0b9ae5a4f8de71bdfdfb28f911b676773d406f86149b0c12ad5fbeb002ad51e42d43ed9f21d41c6ddd1a5b2760c2df58796759f66e7b78ac0dc784cee971
|
|
7
|
+
data.tar.gz: 8dc9e2ff73ddf7d0126538794bb4612a8709881628b713a1e878fe8fba7eddd510597f5a4f0a07e8c9d3b63cc751880df9150db208616f7fc52306ae2474bd10
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,69 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Timeprice::CpiLookup` and `Timeprice::CpiPoint` (Data.define of value +
|
|
12
|
+
granularity). Owns all knowledge of the parsed CPI JSON shape so
|
|
13
|
+
`Inflation.adjust` is a 6-line orchestration.
|
|
14
|
+
- `Timeprice::Sources::Coverage` — isolates runtime filesystem walking
|
|
15
|
+
(FX year scan, JSON.parse of rate files) from the attribution registry.
|
|
16
|
+
- `Timeprice::Point#fx_anchor_date` — resolves a year / month / day `Point`
|
|
17
|
+
to the day-resolved string FX lookup needs (mid-year for `YYYY`,
|
|
18
|
+
mid-month for `YYYY-MM`).
|
|
19
|
+
- `Timeprice::Supported.decimals_for(currency)` — single source of truth
|
|
20
|
+
for ISO 4217 minor-unit counts; non-CLI callers of `Timeprice.exchange`
|
|
21
|
+
can now format results consistently.
|
|
22
|
+
- `Timeprice::CLI::Presenters::{Inflation, Exchange, Compare, Sources}` —
|
|
23
|
+
each presenter exposes `#text_lines` and `#json_hash`; the CLI dispatches
|
|
24
|
+
via a single `#render(presenter)` helper.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- CLI output redesigned for readability: every `inflation`, `fx`, and `compare`
|
|
28
|
+
command now leads with the answer on line 1 (e.g. `3,530,921 VND in 2024`),
|
|
29
|
+
followed by the calculation chain indented below. `head -1` extracts just
|
|
30
|
+
the headline. Numbers are comma-grouped; JSON output is rounded to currency
|
|
31
|
+
precision (no more `1861291.9999999998`).
|
|
32
|
+
- `timeprice sources` now renders as an aligned `ID / SOURCE / LICENSE /
|
|
33
|
+
COVERAGE` table by default. Use `timeprice sources --verbose` (`-v`) for the
|
|
34
|
+
previous detailed view with license URLs and full attribution.
|
|
35
|
+
- Top-level `timeprice help` rewritten — no more truncated descriptions; lists
|
|
36
|
+
command names + descriptions, matching the `git` / `gh` / `cargo` convention.
|
|
37
|
+
- `Point.coerce` rewritten with pattern matching; the CLI's
|
|
38
|
+
`parse_compare_token` now delegates to it instead of re-implementing
|
|
39
|
+
the shape rules.
|
|
40
|
+
- `Compare.resolve_points` uses explicit `raise … unless` guards instead of
|
|
41
|
+
`… || (raise …)` nil-pun.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
- Undocumented back-compat constants: `Timeprice::SUPPORTED_COUNTRIES`,
|
|
45
|
+
`Timeprice::SUPPORTED_CURRENCIES`, and `Timeprice::Compare::CURRENCY_TO_COUNTRY`.
|
|
46
|
+
Use `Supported::COUNTRIES`, `Supported::CURRENCIES`, and
|
|
47
|
+
`Supported::CURRENCY_TO_COUNTRY` directly.
|
|
48
|
+
- `Lint/DuplicateBranch` RuboCop exclusion for `cli.rb` — the duplicate
|
|
49
|
+
was collapsed into a single `rescue Timeprice::Error, ArgumentError`.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- Friendlier error messages: `Error: AMOUNT must be a number, got "abc"`
|
|
53
|
+
instead of Ruby's raw `invalid value for Float(): "abc"`. Missing-options
|
|
54
|
+
errors now say `missing required options: --from, --to` with a `See:
|
|
55
|
+
timeprice help inflation` hint.
|
|
56
|
+
|
|
57
|
+
## [0.2.0] - 2026-05-11
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- `Timeprice::Point` value object for compare inputs; `Point.coerce` accepts `Point` instances or 2-tuples in either `[currency, date]` or `[date, currency]` order.
|
|
61
|
+
- `Timeprice::Supported` module — canonical home for `COUNTRIES`, `CURRENCIES`, and the bidirectional currency↔country map. Replaces the duplicated maps in `Compare` and the CLI's `InflationResult` monkey-patch.
|
|
62
|
+
- `Sources::Base` class extracted from the CPI fetchers; `BLS`, `ONS`, and `Eurostat` now subclass it and implement only `fetch` returning `[monthly, annual]`. The drift-check, rebase, merge, write, and summary-log flow is shared.
|
|
63
|
+
- Per-fetcher GitHub Actions `::warning file=…,title=…::` annotations in `scripts/update_data.rb`, so individual fetcher failures show up on the workflow run with a link to the responsible source file.
|
|
64
|
+
- README "Using from Rails / Rake" section covering service objects, Sidekiq, Rake tasks, and `TIMEPRICE_DATA_ROOT`.
|
|
65
|
+
- YARD documentation on the public API (`Timeprice.{inflation,exchange,compare}`, `Inflation`, `Exchange`, `Compare`, `DataLoader`, `Sources`, error classes, `Supported`, `Point`).
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- `SUPPORTED_COUNTRIES` / `SUPPORTED_CURRENCIES` are now thin aliases for `Supported::COUNTRIES` / `Supported::CURRENCIES`; existing consumers keep working unchanged.
|
|
69
|
+
- `Compare::CURRENCY_TO_COUNTRY` is now an alias for `Supported::CURRENCY_TO_COUNTRY`.
|
|
70
|
+
|
|
8
71
|
## [0.1.2] - 2026-05-11
|
|
9
72
|
|
|
10
73
|
### Added
|
data/README.md
CHANGED
|
@@ -32,16 +32,25 @@ Requires Ruby >= 3.2.
|
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
$ timeprice inflation 100 --from 1990-01 --to 2024-01 --country US
|
|
35
|
-
|
|
35
|
+
242.09 USD in 2024-01
|
|
36
|
+
100.00 USD (1990-01) -> 242.09 USD (2024-01)
|
|
37
|
+
US · monthly CPI
|
|
36
38
|
|
|
37
39
|
$ timeprice fx 100 USD JPY --date 2010-06-15
|
|
38
|
-
|
|
40
|
+
9,118 JPY on 2010-06-15
|
|
41
|
+
100.00 USD -> 9,118 JPY
|
|
42
|
+
rate 91.18
|
|
39
43
|
|
|
40
44
|
$ timeprice compare 100 --from "2010 USD" --to "2024 VND"
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
3,530,921 VND in 2024
|
|
46
|
+
100.00 USD (2010)
|
|
47
|
+
-> fx @ 18,612.92 -> 1,861,292 VND (2010)
|
|
48
|
+
-> inflate x1.8970 VN -> 3,530,921 VND (2024, annual)
|
|
43
49
|
```
|
|
44
50
|
|
|
51
|
+
The first line of each result is the answer — pipe through `head -1` if a
|
|
52
|
+
script only needs the headline figure.
|
|
53
|
+
|
|
45
54
|
Every command supports `--json` for machine-readable output:
|
|
46
55
|
|
|
47
56
|
```bash
|
|
@@ -155,6 +164,91 @@ inflated = Timeprice.inflation(amount: 100, from: "2010", to: "2024", country: "
|
|
|
155
164
|
converted = Timeprice.exchange(amount: inflated, from: "USD", to: "VND", date: "2024-06-30").amount
|
|
156
165
|
```
|
|
157
166
|
|
|
167
|
+
## Using from Rails / Rake
|
|
168
|
+
|
|
169
|
+
`timeprice` is a plain Ruby library — no Railtie, no engine, no autoload magic. It works
|
|
170
|
+
the same way as `BigDecimal` or `JSON`: require it once, call the module functions.
|
|
171
|
+
|
|
172
|
+
### In a Rails app
|
|
173
|
+
|
|
174
|
+
Add the gem to your `Gemfile`:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
gem "timeprice", "~> 0.1"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Then call it directly from controllers, jobs, presenters, or service objects. The library
|
|
181
|
+
is thread-safe (data files are loaded once and cached as frozen hashes), so it's safe to
|
|
182
|
+
call from threaded servers (Puma) and Sidekiq workers:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# app/services/historical_price.rb
|
|
186
|
+
class HistoricalPrice
|
|
187
|
+
def self.in_today_dollars(amount, year)
|
|
188
|
+
Timeprice.inflation(
|
|
189
|
+
amount: amount,
|
|
190
|
+
from: year.to_s,
|
|
191
|
+
to: Date.current.strftime("%Y-%m"),
|
|
192
|
+
country: "US"
|
|
193
|
+
).amount
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Errors all inherit from `Timeprice::Error`, so a single rescue covers everything:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
rescue Timeprice::Error => e
|
|
202
|
+
Rails.logger.warn("timeprice lookup failed: #{e.message}")
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Result objects respond to `#to_h`, so they serialize cleanly in JSON APIs:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
def show
|
|
211
|
+
render json: Timeprice.exchange(amount: 100, from: "USD", to: "EUR", date: params[:date]).to_h
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### In a Rake task
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# lib/tasks/inflation.rake
|
|
219
|
+
require "timeprice"
|
|
220
|
+
|
|
221
|
+
namespace :inflation do
|
|
222
|
+
desc "Print 1990→today inflation for the supported countries"
|
|
223
|
+
task :report do
|
|
224
|
+
today = Date.today.strftime("%Y-%m")
|
|
225
|
+
%w[US UK EU JP VN].each do |c|
|
|
226
|
+
r = Timeprice.inflation(amount: 100, from: "1990", to: today, country: c)
|
|
227
|
+
puts "#{c}: 100 in 1990 → #{r.amount.round(2)} in #{today} (#{r.granularity})"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Configuring the data root
|
|
234
|
+
|
|
235
|
+
By default the gem reads from its bundled `data/` directory. To point at a different
|
|
236
|
+
checkout (useful for testing a new data refresh before releasing it), set
|
|
237
|
+
`TIMEPRICE_DATA_ROOT`:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
TIMEPRICE_DATA_ROOT=/path/to/timeprice/data bundle exec rake inflation:report
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Or programmatically:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
Timeprice::DataLoader.data_root = "/path/to/timeprice/data"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Reassigning `data_root` clears the in-memory cache, so it's safe to call between requests
|
|
250
|
+
in development.
|
|
251
|
+
|
|
158
252
|
## Data sources & attribution
|
|
159
253
|
|
|
160
254
|
`timeprice` redistributes data from several public sources. Each is governed by its own
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../supported"
|
|
4
|
+
|
|
5
|
+
module Timeprice
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
# Number/currency formatting helpers shared by every CLI emitter.
|
|
8
|
+
# Lives as a mixin (rather than a free-standing module function set) so
|
|
9
|
+
# callers can use the helpers as plain methods inside `no_commands` blocks.
|
|
10
|
+
module Formatting
|
|
11
|
+
def fmt_money(amount, currency)
|
|
12
|
+
with_commas(format("%.#{Supported.decimals_for(currency)}f", amount))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Two decimals once we're past the unit threshold; six decimals for
|
|
16
|
+
# sub-unit rates so tiny rates (e.g. 0.000045) still carry signal.
|
|
17
|
+
def fmt_rate(rate)
|
|
18
|
+
decimals = rate.to_f.abs >= 1 ? 2 : 6
|
|
19
|
+
with_commas(format("%.#{decimals}f", rate))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def round_money(amount, currency)
|
|
23
|
+
amount.to_f.round(Supported.decimals_for(currency))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_commas(num_str)
|
|
27
|
+
sign = num_str.start_with?("-") ? "-" : ""
|
|
28
|
+
whole, frac = num_str.sub(/\A-/, "").split(".", 2)
|
|
29
|
+
whole = whole.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
|
|
30
|
+
frac ? "#{sign}#{whole}.#{frac}" : "#{sign}#{whole}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../formatting"
|
|
4
|
+
|
|
5
|
+
module Timeprice
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
module Presenters
|
|
8
|
+
# Renders a CompareResult for the CLI in text and JSON formats.
|
|
9
|
+
class Compare
|
|
10
|
+
include Formatting
|
|
11
|
+
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def json_hash
|
|
17
|
+
@result.to_h.merge(
|
|
18
|
+
amount: round_money(@result.amount, @result.to_currency),
|
|
19
|
+
original_amount: round_money(@result.original_amount, @result.from_currency),
|
|
20
|
+
converted_amount: round_money(@result.converted_amount, @result.to_currency),
|
|
21
|
+
fx_rate: @result.fx_rate.to_f.round(6),
|
|
22
|
+
cpi_ratio: @result.cpi_ratio.to_f.round(6)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Headline + left-to-right chain so the FX + CPI composition reads naturally.
|
|
27
|
+
def text_lines
|
|
28
|
+
final = "#{fmt_money(@result.amount, @result.to_currency)} #{@result.to_currency}"
|
|
29
|
+
original = "#{fmt_money(@result.original_amount, @result.from_currency)} #{@result.from_currency}"
|
|
30
|
+
converted = "#{fmt_money(@result.converted_amount, @result.to_currency)} #{@result.to_currency}"
|
|
31
|
+
step1 = "fx @ #{fmt_rate(@result.fx_rate)}"
|
|
32
|
+
step2 = "inflate x#{format("%.4f", @result.cpi_ratio)} #{@result.country}"
|
|
33
|
+
width = [step1.length, step2.length].max
|
|
34
|
+
[
|
|
35
|
+
"#{final} in #{@result.to_date}",
|
|
36
|
+
" #{original} (#{@result.from_date})",
|
|
37
|
+
format(" -> %-#{width}s -> %s (%s)", step1, converted, @result.from_date),
|
|
38
|
+
format(" -> %-#{width}s -> %s (%s, %s)", step2, final, @result.to_date, @result.granularity),
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../formatting"
|
|
4
|
+
|
|
5
|
+
module Timeprice
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
module Presenters
|
|
8
|
+
# Renders an ExchangeResult for the CLI in text and JSON formats.
|
|
9
|
+
class Exchange
|
|
10
|
+
include Formatting
|
|
11
|
+
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def json_hash
|
|
17
|
+
@result.to_h.merge(
|
|
18
|
+
amount: round_money(@result.amount, @result.to),
|
|
19
|
+
original_amount: round_money(@result.original_amount, @result.from),
|
|
20
|
+
rate: @result.rate.to_f.round(6)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def text_lines
|
|
25
|
+
[
|
|
26
|
+
"#{fmt_money(@result.amount, @result.to)} #{@result.to} on #{@result.date}",
|
|
27
|
+
format(" %s %s -> %s %s",
|
|
28
|
+
fmt_money(@result.original_amount, @result.from), @result.from,
|
|
29
|
+
fmt_money(@result.amount, @result.to), @result.to),
|
|
30
|
+
" #{rate_line}",
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def rate_line
|
|
37
|
+
line = "rate #{fmt_rate(@result.rate)}"
|
|
38
|
+
return line unless @result.effective_date && @result.effective_date != @result.date
|
|
39
|
+
|
|
40
|
+
"#{line} from #{@result.effective_date} (fallback)"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../formatting"
|
|
4
|
+
|
|
5
|
+
module Timeprice
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
module Presenters
|
|
8
|
+
# Renders an InflationResult for the CLI in text and JSON formats.
|
|
9
|
+
class Inflation
|
|
10
|
+
include Formatting
|
|
11
|
+
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
@ccy = result.country_currency_label
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def json_hash
|
|
18
|
+
@result.to_h.merge(
|
|
19
|
+
amount: round_money(@result.amount, @ccy),
|
|
20
|
+
original_amount: round_money(@result.original_amount, @ccy)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def text_lines
|
|
25
|
+
[
|
|
26
|
+
"#{fmt_money(@result.amount, @ccy)} #{@ccy} in #{@result.to}",
|
|
27
|
+
format(" %s %s (%s) -> %s %s (%s)",
|
|
28
|
+
fmt_money(@result.original_amount, @ccy), @ccy, @result.from,
|
|
29
|
+
fmt_money(@result.amount, @ccy), @ccy, @result.to),
|
|
30
|
+
" #{@result.country} · #{@result.granularity} CPI",
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Timeprice
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
module Presenters
|
|
6
|
+
# Renders the sources list in compact-table, verbose, and JSON formats.
|
|
7
|
+
class Sources
|
|
8
|
+
MAX_SOURCE_NAME = 60
|
|
9
|
+
|
|
10
|
+
def initialize(list, verbose: false)
|
|
11
|
+
@list = list
|
|
12
|
+
@verbose = verbose
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def json_hash
|
|
16
|
+
@list
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def text_lines
|
|
20
|
+
@verbose ? verbose_lines : table_lines
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def table_lines
|
|
26
|
+
rows = @list.map do |s|
|
|
27
|
+
[s[:id].to_s, short_source_name(s[:name]), s[:license].to_s, s[:coverage].to_s]
|
|
28
|
+
end
|
|
29
|
+
headers = %w[ID SOURCE LICENSE COVERAGE]
|
|
30
|
+
widths = headers.each_with_index.map { |h, i| [h.length, *rows.map { |r| r[i].length }].max }
|
|
31
|
+
fmt = " %-#{widths[0]}s %-#{widths[1]}s %-#{widths[2]}s %s"
|
|
32
|
+
[
|
|
33
|
+
format(fmt, *headers),
|
|
34
|
+
*rows.map { |r| format(fmt, *r) },
|
|
35
|
+
"",
|
|
36
|
+
"Run `timeprice sources --verbose` for license URLs and full attribution.",
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def verbose_lines
|
|
41
|
+
@list.flat_map do |s|
|
|
42
|
+
[
|
|
43
|
+
s[:name].to_s,
|
|
44
|
+
" id: #{s[:id]}",
|
|
45
|
+
" license: #{s[:license]}",
|
|
46
|
+
" license_url: #{s[:license_url]}",
|
|
47
|
+
" attribution: #{s[:attribution]}",
|
|
48
|
+
" coverage: #{s[:coverage]}",
|
|
49
|
+
"",
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Cap the source-name column width. Truncation is last resort — the full
|
|
55
|
+
# name (with series code) is preserved in `--verbose` output.
|
|
56
|
+
def short_source_name(name)
|
|
57
|
+
s = name.to_s
|
|
58
|
+
return s if s.length <= MAX_SOURCE_NAME
|
|
59
|
+
|
|
60
|
+
"#{s[0, MAX_SOURCE_NAME - 1]}…"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|