git-fit 0.5.9 → 0.6.1
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/lib/git-fit.rb +5 -0
- data/lib/git_fit/cli/sync.rb +50 -0
- data/lib/git_fit/cli.rb +8 -0
- data/lib/git_fit/config_template.rb +3 -0
- data/lib/git_fit/db/rebuild.rb +12 -0
- data/lib/git_fit/source.rb +3 -1
- data/lib/git_fit/sync/base.rb +148 -1
- data/lib/git_fit/sync/lorem.rb +124 -0
- data/lib/git_fit/util/tally.rb +92 -0
- data/lib/git_fit/util.rb +4 -0
- data/lib/git_fit/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75362a4faf88f3ca8497484ba835358eb06d82e36d81ad3e53d7dc4ec877f478
|
|
4
|
+
data.tar.gz: 6cd85cd677977d84670daa160235ff65b5a7ebad55b8cd51ea59cd5661a6bd8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3aba69713ec7ba93ead186be8b58fbe577186b83a47ccded9c0f872abe492f15ee5337577e7c4aa36c07f7e6d33588d50231001e85e083156f79324bcf5daca1
|
|
7
|
+
data.tar.gz: 0d8e9e012c911e5609d00be8324995acef30f3a3ff2ba44d9e15380881c1c7882226074d70c5723953b42a0535bf4ec21b329d26fc78d4a9b0f51ca45766a820
|
data/lib/git-fit.rb
CHANGED
|
@@ -57,6 +57,11 @@ require_relative "git_fit/privacy/polyline_filter"
|
|
|
57
57
|
require_relative "git_fit/import"
|
|
58
58
|
require_relative "git_fit/sync/base"
|
|
59
59
|
require_relative "git_fit/sync/strava"
|
|
60
|
+
require_relative "git_fit/util"
|
|
61
|
+
require_relative "git_fit/sync/base"
|
|
62
|
+
require_relative "git_fit/sync/strava"
|
|
63
|
+
require_relative "git_fit/sync/lorem"
|
|
64
|
+
require_relative "git_fit/cli/sync"
|
|
60
65
|
require_relative "git_fit/db/rebuild"
|
|
61
66
|
require_relative "git_fit/db/connection"
|
|
62
67
|
require_relative "git_fit/db/activity"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module GitFit
|
|
2
|
+
module Cli
|
|
3
|
+
module Sync
|
|
4
|
+
def sync
|
|
5
|
+
db = connect_db
|
|
6
|
+
source = options[:source]
|
|
7
|
+
unless source
|
|
8
|
+
say_status :error, "Use --source to specify a source (e.g. lorem)", :red
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
filter = options[:activity]&.flat_map { |v| v.split(",") }&.map(&:strip)&.map(&:downcase)
|
|
13
|
+
privacy = GitFit::Privacy::PolylineFilter.new(config: git_fit_config.privacy_config)
|
|
14
|
+
adapter = build_sync_adapter(source, db, filter, privacy: privacy)
|
|
15
|
+
unless adapter
|
|
16
|
+
say_status :error, "Unknown source: #{source}", :red
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
say_status :syncing, source, :green
|
|
21
|
+
count = adapter.call
|
|
22
|
+
if count > 0
|
|
23
|
+
say_status :done, "#{source}: #{count} activities synced", :green
|
|
24
|
+
else
|
|
25
|
+
say_status :warn, "#{source}: 0 activities", :yellow
|
|
26
|
+
end
|
|
27
|
+
rescue => e
|
|
28
|
+
say_status :error, "#{source}: #{e.message}", :red
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def source_configured?(source)
|
|
34
|
+
return true if GitFit::Sync.const_defined?(source.capitalize)
|
|
35
|
+
config = git_fit_config
|
|
36
|
+
cfg = config.sync_config(source)
|
|
37
|
+
cfg && !cfg.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_sync_adapter(source, db, filter, privacy: nil)
|
|
41
|
+
case source
|
|
42
|
+
when "lorem"
|
|
43
|
+
GitFit::Sync::Lorem.new(config: git_fit_config.sync_config(source), db: db, activity_filter: filter, privacy: privacy)
|
|
44
|
+
else
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/git_fit/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "thor"
|
|
|
3
3
|
module GitFit
|
|
4
4
|
class CLI < Thor
|
|
5
5
|
package_name "git-fit"
|
|
6
|
+
include Cli::Sync
|
|
6
7
|
|
|
7
8
|
class_option :config, type: :string, desc: "Config file path", aliases: "-c"
|
|
8
9
|
|
|
@@ -61,6 +62,13 @@ module GitFit
|
|
|
61
62
|
say_status :error, "Parse failed: #{e.message}", :red
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
desc "sync", "Sync workout data from a source"
|
|
66
|
+
option :source, type: :string, desc: "Source name (e.g. lorem)"
|
|
67
|
+
option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
|
|
68
|
+
def sync
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
desc "gh SUBCOMMAND", "Manage GitHub secrets and variables"
|
|
65
73
|
subcommand "gh", GhCLI
|
|
66
74
|
|
data/lib/git_fit/db/rebuild.rb
CHANGED
|
@@ -8,6 +8,10 @@ module GitFit
|
|
|
8
8
|
def self.call(db, db_path, migrations_path)
|
|
9
9
|
backup = "#{db_path}.rebuild_bak"
|
|
10
10
|
FileUtils.cp(db_path, backup)
|
|
11
|
+
%w[-wal -shm].each do |ext|
|
|
12
|
+
src = "#{db_path}#{ext}"
|
|
13
|
+
FileUtils.cp(src, "#{backup}#{ext}") if File.exist?(src)
|
|
14
|
+
end
|
|
11
15
|
|
|
12
16
|
tables = db.tables - [:schema_migrations]
|
|
13
17
|
data = {}
|
|
@@ -29,8 +33,16 @@ module GitFit
|
|
|
29
33
|
rescue => e
|
|
30
34
|
if backup && File.exist?(backup)
|
|
31
35
|
FileUtils.cp(backup, db_path)
|
|
36
|
+
%w[-wal -shm].each do |ext|
|
|
37
|
+
src = "#{backup}#{ext}"
|
|
38
|
+
FileUtils.cp(src, "#{db_path}#{ext}") if File.exist?(src)
|
|
39
|
+
end
|
|
32
40
|
end
|
|
33
41
|
raise RebuildError, "Rebuild failed: #{e.message}"
|
|
42
|
+
ensure
|
|
43
|
+
[backup, "#{backup}-wal", "#{backup}-shm"].each do |f|
|
|
44
|
+
FileUtils.rm_f(f)
|
|
45
|
+
end
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def self.export_table(db, table)
|
data/lib/git_fit/source.rb
CHANGED
data/lib/git_fit/sync/base.rb
CHANGED
|
@@ -1,9 +1,78 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "time"
|
|
3
|
+
|
|
1
4
|
module GitFit
|
|
2
5
|
module Sync
|
|
3
6
|
class Base
|
|
7
|
+
def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil)
|
|
8
|
+
@config = config
|
|
9
|
+
@db = db
|
|
10
|
+
@activity_filter = activity_filter
|
|
11
|
+
@privacy = privacy
|
|
12
|
+
@time_budget = time_budget
|
|
13
|
+
@start_time = nil
|
|
14
|
+
@progress_reported = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :progress_reported
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
return 0 unless before_call
|
|
21
|
+
return 0 unless respond_to?(:authenticate) ? authenticate : true
|
|
22
|
+
ids = pending_ids
|
|
23
|
+
return 0 if ids.nil? || ids.empty?
|
|
24
|
+
total = ids.size
|
|
25
|
+
count = 0
|
|
26
|
+
ids.filter_map do |p|
|
|
27
|
+
next if skip_type?(p[:type])
|
|
28
|
+
r = process_one(p)
|
|
29
|
+
count += 1
|
|
30
|
+
report_progress(count, total, build_progress_info(r)) if r
|
|
31
|
+
r
|
|
32
|
+
end.size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def before_call
|
|
36
|
+
@start_time = Time.now
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pending_ids
|
|
41
|
+
raise NotImplementedError, "#{self.class} must implement #pending_ids"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def process_one(pending)
|
|
45
|
+
raise NotImplementedError, "#{self.class} must implement #process_one"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_progress_info(attrs)
|
|
49
|
+
parts = []
|
|
50
|
+
sd = attrs["start_date"] || attrs[:start_date]
|
|
51
|
+
if sd
|
|
52
|
+
d = Date.parse(sd.to_s) rescue nil
|
|
53
|
+
if d
|
|
54
|
+
date_str = d.year == Date.today.year ? d.strftime("%m-%d") : d.strftime("%Y-%m-%d")
|
|
55
|
+
parts << date_str
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
type = attrs["sport_category"] || attrs[:sport_category]
|
|
59
|
+
parts << type if type
|
|
60
|
+
dist = attrs["distance"] || attrs[:distance]
|
|
61
|
+
if dist && dist.to_f > 0
|
|
62
|
+
parts << "#{format("%.1f", dist.to_f / 1000)}km"
|
|
63
|
+
end
|
|
64
|
+
source = attrs["source"] || attrs[:source] || source_label
|
|
65
|
+
name = attrs["name"] || attrs[:name]
|
|
66
|
+
display_name = name.to_s.strip
|
|
67
|
+
display_name = "#{source} Activity" if display_name.empty?
|
|
68
|
+
chars = display_name.chars
|
|
69
|
+
display_name = chars.first(30).join + "…" if chars.size > 30
|
|
70
|
+
parts << display_name
|
|
71
|
+
parts.join(" | ") unless parts.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
4
74
|
def self.register_config(*keys)
|
|
5
75
|
return if keys.empty?
|
|
6
|
-
|
|
7
76
|
source = name.split("::").last.downcase
|
|
8
77
|
GitFit.register_config_source(
|
|
9
78
|
prefix: "GIT_FIT_#{source.upcase}",
|
|
@@ -11,6 +80,84 @@ module GitFit
|
|
|
11
80
|
config_path: ["sync", source]
|
|
12
81
|
)
|
|
13
82
|
end
|
|
83
|
+
|
|
84
|
+
protected
|
|
85
|
+
|
|
86
|
+
def raw_dir
|
|
87
|
+
src = self.class.name.split("::").last.downcase
|
|
88
|
+
File.join("data", "raw", src)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def std_dir
|
|
92
|
+
src = self.class.name.split("::").last.downcase
|
|
93
|
+
File.join("data", "std", src)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def std_path(platform_id, ext = "json")
|
|
97
|
+
File.join(std_dir, "#{platform_id}.#{ext}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def upsert_activity(attrs)
|
|
101
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
102
|
+
now = Time.now.utc
|
|
103
|
+
|
|
104
|
+
if @privacy && !@privacy.before_saving?
|
|
105
|
+
attrs["summary_polyline"] = @privacy.filter(attrs["summary_polyline"])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
existing = @db[:activities].where(run_id: attrs["run_id"]).first
|
|
109
|
+
if existing
|
|
110
|
+
attrs.delete("created_at")
|
|
111
|
+
attrs["updated_at"] = now
|
|
112
|
+
@db[:activities].where(run_id: attrs["run_id"]).update(attrs)
|
|
113
|
+
else
|
|
114
|
+
attrs["created_at"] ||= now
|
|
115
|
+
attrs["updated_at"] ||= now
|
|
116
|
+
@db[:activities].insert(attrs)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def skip_type?(sport_type)
|
|
121
|
+
return false unless @activity_filter
|
|
122
|
+
!@activity_filter.include?(sport_type.to_s.downcase)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def report_progress(count, total = nil, info = nil)
|
|
126
|
+
return unless show_progress?(count, total)
|
|
127
|
+
@progress_reported = true
|
|
128
|
+
line = "#{source_label}: #{count}"
|
|
129
|
+
line += "/#{total}" if total
|
|
130
|
+
line += " #{info}" if info
|
|
131
|
+
line += " "
|
|
132
|
+
print "\r#{line}"
|
|
133
|
+
$stdout.flush
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def source_label
|
|
137
|
+
name = self.class.name
|
|
138
|
+
name ? name.split("::").last : "Adapter"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def time_expired?
|
|
142
|
+
return false unless @time_budget.is_a?(Numeric) && @time_budget > 0 && @start_time
|
|
143
|
+
(Time.now - @start_time) > @time_budget
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def raw_file_exists?(platform_id)
|
|
147
|
+
File.exist?(raw_path(platform_id))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def show_progress?(count, total)
|
|
151
|
+
return true if count == 1
|
|
152
|
+
return true if total && (total - count) <= 50
|
|
153
|
+
step = progress_step(total)
|
|
154
|
+
step <= 1 || count % step == 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def progress_step(total)
|
|
158
|
+
return 1 unless total && total > 0
|
|
159
|
+
[1, (1.5 * (total ** 0.4)).to_i].max
|
|
160
|
+
end
|
|
14
161
|
end
|
|
15
162
|
end
|
|
16
163
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module GitFit
|
|
6
|
+
module Sync
|
|
7
|
+
class Lorem < Base
|
|
8
|
+
register_config :count
|
|
9
|
+
|
|
10
|
+
DEMO_ACTIVITIES = [
|
|
11
|
+
{ id: "alpha_001", name: "Lorem Morning Run", sport_category: "run",
|
|
12
|
+
distance: 5200.0, moving_time: 1800, elapsed_time: 2000,
|
|
13
|
+
start_date: "2025-01-15T06:30:00Z", start_date_local: "2025-01-15T14:30:00+08:00",
|
|
14
|
+
location_country: "CN", average_heartrate: 148.0, max_heartrate: 165.0,
|
|
15
|
+
average_cadence: 172.0, max_cadence: 185.0, average_power: nil, max_power: nil,
|
|
16
|
+
average_temperature: 22.0, calories: 380,
|
|
17
|
+
average_speed: 5200.0 / 1800, elevation_gain: 125.0, external_id: "lorem_001" },
|
|
18
|
+
{ id: "beta_002", name: "Ipsum Cycling Session", sport_category: "ride",
|
|
19
|
+
distance: 35000.0, moving_time: 5400, elapsed_time: 6000,
|
|
20
|
+
start_date: "2025-01-14T08:00:00Z", start_date_local: "2025-01-14T16:00:00+08:00",
|
|
21
|
+
location_country: "CN", average_heartrate: 138.0, max_heartrate: 155.0,
|
|
22
|
+
average_cadence: 85.0, max_cadence: 105.0, average_power: 185.0, max_power: 420.0,
|
|
23
|
+
average_temperature: 18.0, calories: 850,
|
|
24
|
+
average_speed: 35000.0 / 5400, elevation_gain: 450.0, external_id: "lorem_002" },
|
|
25
|
+
{ id: "gamma_003", name: "Dolor Hike", sport_category: "hike",
|
|
26
|
+
distance: 12000.0, moving_time: 7200, elapsed_time: 8100,
|
|
27
|
+
start_date: "2025-01-13T09:00:00Z", start_date_local: "2025-01-13T17:00:00+08:00",
|
|
28
|
+
location_country: "CN", average_heartrate: 122.0, max_heartrate: 142.0,
|
|
29
|
+
average_cadence: 110.0, max_cadence: 135.0, average_power: nil, max_power: nil,
|
|
30
|
+
average_temperature: 15.0, calories: 620,
|
|
31
|
+
average_speed: 12000.0 / 7200, elevation_gain: 820.0, external_id: "lorem_003" },
|
|
32
|
+
{ id: "delta_004", name: "Sit Amet Walk", sport_category: "walk",
|
|
33
|
+
distance: 5000.0, moving_time: 3600, elapsed_time: 3900,
|
|
34
|
+
start_date: "2025-01-12T18:00:00Z", start_date_local: "2025-01-12T18:00:00+08:00",
|
|
35
|
+
location_country: "CN", average_heartrate: 108.0, max_heartrate: 125.0,
|
|
36
|
+
average_cadence: 105.0, max_cadence: 118.0, average_power: nil, max_power: nil,
|
|
37
|
+
average_temperature: 20.0, calories: 280,
|
|
38
|
+
average_speed: 5000.0 / 3600, elevation_gain: 35.0, external_id: "lorem_004" },
|
|
39
|
+
{ id: "epsilon_005", name: "Consectetur Swim", sport_category: "swim",
|
|
40
|
+
distance: 2000.0, moving_time: 2400, elapsed_time: 2700,
|
|
41
|
+
start_date: "2025-01-11T07:00:00Z", start_date_local: "2025-01-11T15:00:00+08:00",
|
|
42
|
+
location_country: "CN", average_heartrate: 135.0, max_heartrate: 150.0,
|
|
43
|
+
average_cadence: 28.0, max_cadence: 32.0, average_power: nil, max_power: nil,
|
|
44
|
+
average_temperature: 26.0, calories: 520,
|
|
45
|
+
average_speed: 2000.0 / 2400, elevation_gain: 0.0, external_id: "lorem_005" }
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
def authenticate
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pending_ids
|
|
53
|
+
DEMO_ACTIVITIES.map { |act| { id: act[:id], type: act[:sport_category], summary: act } }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def process_one(pending)
|
|
57
|
+
attrs = build_attrs(pending[:summary])
|
|
58
|
+
|
|
59
|
+
dir = raw_dir
|
|
60
|
+
FileUtils.mkdir_p(dir)
|
|
61
|
+
File.write(File.join(dir, "#{pending[:id]}.json"), JSON.generate(pending[:summary]))
|
|
62
|
+
|
|
63
|
+
upsert_activity(attrs)
|
|
64
|
+
attrs
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def raw_path(platform_id)
|
|
68
|
+
File.join(raw_dir, "#{platform_id}.json")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def build_attrs(act)
|
|
74
|
+
coords = generate_route_coords(act[:sport_category], act[:distance])
|
|
75
|
+
polyline = Geo::Polyline.encode(coords)
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
run_id: "lorem_#{act[:id]}",
|
|
79
|
+
name: act[:name],
|
|
80
|
+
distance: act[:distance],
|
|
81
|
+
moving_time: act[:moving_time],
|
|
82
|
+
elapsed_time: act[:elapsed_time],
|
|
83
|
+
sport_category: act[:sport_category],
|
|
84
|
+
sport_type: act[:sport_category],
|
|
85
|
+
start_date: Time.parse(act[:start_date])&.iso8601,
|
|
86
|
+
start_date_local: act[:start_date_local],
|
|
87
|
+
location_country: act[:location_country],
|
|
88
|
+
summary_polyline: polyline,
|
|
89
|
+
average_heartrate: act[:average_heartrate],
|
|
90
|
+
max_heartrate: act[:max_heartrate],
|
|
91
|
+
average_cadence: act[:average_cadence],
|
|
92
|
+
max_cadence: act[:max_cadence],
|
|
93
|
+
average_power: act[:average_power],
|
|
94
|
+
max_power: act[:max_power],
|
|
95
|
+
average_temperature: act[:average_temperature],
|
|
96
|
+
calories: act[:calories],
|
|
97
|
+
average_speed: act[:average_speed],
|
|
98
|
+
elevation_gain: act[:elevation_gain],
|
|
99
|
+
source: "lorem",
|
|
100
|
+
external_id: act[:external_id]
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def generate_route_coords(category, distance)
|
|
105
|
+
base_lat = 31.2304
|
|
106
|
+
base_lng = 121.4737
|
|
107
|
+
num_points = [(distance / 500.0).ceil, 20].max
|
|
108
|
+
|
|
109
|
+
case category
|
|
110
|
+
when "run"
|
|
111
|
+
(0...num_points).map { |i| [base_lat + i * 0.0002, base_lng + i * 0.0003] }
|
|
112
|
+
when "ride"
|
|
113
|
+
(0...num_points).map { |i| [base_lat + i * 0.0005, base_lng + i * 0.0008] }
|
|
114
|
+
when "hike"
|
|
115
|
+
(0...num_points).map { |i| [base_lat + i * 0.0003 + Math.sin(i * 0.5) * 0.0001, base_lng + i * 0.0004] }
|
|
116
|
+
when "swim"
|
|
117
|
+
[[base_lat, base_lng]]
|
|
118
|
+
else
|
|
119
|
+
(0...num_points).map { |i| [base_lat + i * 0.0002, base_lng + i * 0.0002] }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module GitFit
|
|
4
|
+
module Util
|
|
5
|
+
class Tally
|
|
6
|
+
DIGITS = %w[零 一 二 三 四 五 六 七 八 九 十].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(count = 0)
|
|
11
|
+
@value = count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(n = 1)
|
|
15
|
+
@value += n
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset(count = 0)
|
|
20
|
+
@value = count
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_i
|
|
25
|
+
@value.to_i
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_int
|
|
29
|
+
to_i
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ==(other)
|
|
33
|
+
to_i == other.to_i
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def <(other)
|
|
37
|
+
to_i < other.to_i
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s(format: :chinese)
|
|
41
|
+
case format
|
|
42
|
+
when :arabic then @value.to_s
|
|
43
|
+
else to_chinese
|
|
44
|
+
end
|
|
45
|
+
rescue StandardError
|
|
46
|
+
@value.to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect
|
|
50
|
+
"#<#{self.class.name} value=#{@value}>"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def to_chinese
|
|
56
|
+
return @value.to_s if @value > 9999
|
|
57
|
+
chinese_numeral
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def chinese_numeral
|
|
61
|
+
n = @value
|
|
62
|
+
return DIGITS[0] if n == 0
|
|
63
|
+
return DIGITS[n] if n <= 10
|
|
64
|
+
|
|
65
|
+
result = ""
|
|
66
|
+
thousands = n / 1000
|
|
67
|
+
n %= 1000
|
|
68
|
+
result += DIGITS[thousands] + "千" if thousands > 0
|
|
69
|
+
|
|
70
|
+
hundreds = n / 100
|
|
71
|
+
n %= 100
|
|
72
|
+
if hundreds > 0
|
|
73
|
+
result += DIGITS[hundreds] + "百"
|
|
74
|
+
elsif thousands > 0 && n > 0
|
|
75
|
+
result += "零"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
tens = n / 10
|
|
79
|
+
ones = n % 10
|
|
80
|
+
if tens > 0
|
|
81
|
+
join_ten = tens > 1 || !result.empty?
|
|
82
|
+
result += (join_ten ? DIGITS[tens] : "") + "十"
|
|
83
|
+
elsif !result.empty? && tens == 0 && ones > 0 && !result.end_with?("零")
|
|
84
|
+
result += "零"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
result += DIGITS[ones] if ones > 0
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/git_fit/util.rb
ADDED
data/lib/git_fit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-fit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lax
|
|
@@ -137,6 +137,7 @@ files:
|
|
|
137
137
|
- lib/git_fit/cli/gh_cli.rb
|
|
138
138
|
- lib/git_fit/cli/import_cli.rb
|
|
139
139
|
- lib/git_fit/cli/install_cli.rb
|
|
140
|
+
- lib/git_fit/cli/sync.rb
|
|
140
141
|
- lib/git_fit/config.rb
|
|
141
142
|
- lib/git_fit/config_template.rb
|
|
142
143
|
- lib/git_fit/db/activity.rb
|
|
@@ -166,8 +167,11 @@ files:
|
|
|
166
167
|
- lib/git_fit/source/tally.rb
|
|
167
168
|
- lib/git_fit/sport_mapper.rb
|
|
168
169
|
- lib/git_fit/sync/base.rb
|
|
170
|
+
- lib/git_fit/sync/lorem.rb
|
|
169
171
|
- lib/git_fit/sync/strava.rb
|
|
170
172
|
- lib/git_fit/timezone/resolver.rb
|
|
173
|
+
- lib/git_fit/util.rb
|
|
174
|
+
- lib/git_fit/util/tally.rb
|
|
171
175
|
- lib/git_fit/version.rb
|
|
172
176
|
homepage: https://github.com/Lax/git-fit
|
|
173
177
|
licenses:
|