gopromotional-siplog 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c77cd57e02d0c41931dd6367c603da79ebed4e63bd203f210012106ee73033a1
4
+ data.tar.gz: f84acc29f6379dd680ebbbb02f09b673256a83a0f03b2d8f20e392ea4b4faeaa
5
+ SHA512:
6
+ metadata.gz: 3f69688b133665d606eeef617269a9ccda4529be668755623224afc9f2705a8e750d71c5872361e7c40d8534aaeb9437a1c1e85c039faaa3fa03857ddca2a969
7
+ data.tar.gz: c9eeda9d8482e3d33e050db35444783ed0e47d564f97966c3df08cdc9578ad46cbeb1547894a2f16d9bc8bb809c0333b8293319564adec0c42abd302e02aa3c5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GoPromotional
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # gopromotional-siplog
2
+
3
+ A small hydration tracker: estimate a daily water goal from weight, exercise and weather, log drinks in ml, cl, litres or US fl oz, and see today's progress, the last 7 days, your average and your streak. The log is a plain JSON file.
4
+
5
+ - **Daily goal** estimate: weight × 33 ml + 12 ml per active minute (+500 ml in hot weather), rounded to 50 ml. A general guide, not medical advice.
6
+ - **Flexible amounts**: `250`, `500ml`, `33cl`, `0.5l`, `16oz`.
7
+ - **Progress**, **7-day history**, **average** and **streak** of days on target.
8
+ - **Undo** the last entry, **set** your own goal.
9
+ - Plain **JSON log** (`{goal, entries:[{date, ml}]}`), the same format in every language version.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ gem install gopromotional-siplog
15
+ # or in a Gemfile:
16
+ gem "gopromotional-siplog"
17
+ ```
18
+
19
+ ## Command line
20
+
21
+ ```console
22
+ $ gopromotional-siplog goal --weight 70 --activity 30 --hot
23
+ Suggested daily water goal: 3150 ml (a general guide, not medical advice).
24
+
25
+ $ gopromotional-siplog set-goal 2.5l
26
+ Daily goal set to 2500 ml.
27
+
28
+ $ gopromotional-siplog add 500ml --date 2026-10-19
29
+ Added 500 ml. 500/2500 ml (20%).
30
+
31
+ $ gopromotional-siplog add 16oz --date 2026-10-19
32
+ Added 473 ml. 973/2500 ml (39%).
33
+
34
+ $ gopromotional-siplog status --date 2026-10-19
35
+ 2026-10-19: 973/2500 ml [########............] 39% - 1527 ml to go
36
+ Streak: 0 day(s)
37
+ ```
38
+
39
+ Every command accepts `--json` for machine-readable output. Run `gopromotional-siplog --help` for all options.
40
+
41
+ Data is kept in `~/.gopromotional-siplog.json` (set `GOPROMOTIONAL_SIPLOG_FILE` or pass `--file` to use another file). The JSON format is the same across the npm, PyPI, crates.io and RubyGems versions, so you can switch tools without losing your log.
42
+
43
+ ## Library
44
+
45
+ ```ruby
46
+ require "gopromotional/siplog"
47
+ S = GoPromotional::Siplog
48
+
49
+ S.daily_goal(weight_kg: 70, activity_minutes: 30, hot: true) # => 3150
50
+ S.parse_amount("16oz") # => 473
51
+ log = S::HydrationLog.new(goal: 2000)
52
+ log.add("500ml", "2026-10-19"); log.add(250, "2026-10-19")
53
+ log.progress("2026-10-19")[:percent] # => 38
54
+ ```
55
+
56
+ Pure Ruby, no runtime dependencies, Ruby 3.0+. `require "gopromotional-siplog"` also works.
57
+
58
+ ## Links
59
+
60
+ - Website: [branded water bottles for event marketing](https://www.gopromotional.co.uk/branded-products/branded-water-bottles)
61
+ - Source code: [https://github.com/nbkdevs/GoPromotional](https://github.com/nbkdevs/GoPromotional) (`packages/water-bottles/rubygems`)
62
+ - Licence: MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/gopromotional/siplog/cli"
5
+ exit GoPromotional::Siplog::CLI.run(ARGV)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../siplog"
5
+
6
+ module GoPromotional
7
+ module Siplog
8
+ # Command line interface: goal | add | status | week | undo | set-goal
9
+ module CLI
10
+ USAGE = <<~TXT
11
+ Usage: gopromotional-siplog <command> [options]
12
+
13
+ Commands:
14
+ goal --weight KG [--activity MIN] [--hot] suggest a daily water goal
15
+ add <amount> log a drink: 250, 500ml, 0.5l, 33cl, 16oz
16
+ status today's progress and streak
17
+ week last 7 days and average
18
+ undo remove the last entry for the day
19
+ set-goal <ml> set the daily goal
20
+
21
+ Options: --file PATH (default $GOPROMOTIONAL_SIPLOG_FILE or ~/.gopromotional-siplog.json),
22
+ --date YYYY-MM-DD, --json, -v/--version, -h/--help
23
+ TXT
24
+
25
+ module_function
26
+
27
+ def default_file
28
+ env = ENV.fetch("GOPROMOTIONAL_SIPLOG_FILE", "")
29
+ env.empty? ? File.join(Dir.home, ".gopromotional-siplog.json") : env
30
+ end
31
+
32
+ def bar(pct)
33
+ ("#" * [20, (pct / 5.0 + 0.5).to_i].min).ljust(20, ".")
34
+ end
35
+
36
+ def run(argv, out: $stdout, err: $stderr)
37
+ o = { json: false, activity: 0.0, hot: false }
38
+ parser = OptionParser.new do |p|
39
+ p.banner = USAGE
40
+ p.on("--json") { o[:json] = true }
41
+ p.on("--file PATH") { |v| o[:file] = v }
42
+ p.on("--date DATE") { |v| o[:date] = v }
43
+ p.on("--weight KG", Float) { |v| o[:weight] = v }
44
+ p.on("--activity MIN", Float) { |v| o[:activity] = v }
45
+ p.on("--hot") { o[:hot] = true }
46
+ p.on("-v", "--version") { out.puts VERSION; return 0 }
47
+ p.on("-h", "--help") { out.puts USAGE; return 0 }
48
+ end
49
+ args = parser.parse(argv)
50
+ cmd = args.shift
51
+ (out.puts USAGE; return 0) if cmd.nil?
52
+ day = o[:date] || Date.today.iso8601
53
+ path = o[:file] || default_file
54
+ if cmd == "goal"
55
+ raise ArgumentError, "goal needs --weight KG" unless o[:weight]
56
+ r = { goal: Siplog.daily_goal(weight_kg: o[:weight], activity_minutes: o[:activity], hot: o[:hot]) }
57
+ text = "Suggested daily water goal: #{r[:goal]} ml (a general guide, not medical advice)."
58
+ else
59
+ log = File.exist?(path) ? HydrationLog.from_json(File.read(path, encoding: "utf-8")) : HydrationLog.new
60
+ save = -> { File.write(path, "#{log.to_json}\n") }
61
+ case cmd
62
+ when "add"
63
+ raise ArgumentError, "add needs an amount, e.g. 250 or 500ml" unless args.first
64
+ ml = log.add(args.join(" "), day).ml
65
+ save.call
66
+ r = log.progress(day)
67
+ text = "Added #{ml} ml. #{r[:total]}/#{r[:goal]} ml (#{r[:percent]}%)."
68
+ when "status"
69
+ r = log.progress(day)
70
+ text = "#{r[:date]}: #{r[:total]}/#{r[:goal]} ml [#{bar(r[:percent])}] #{r[:percent]}%" +
71
+ (r[:reached] ? " - goal reached" : " - #{r[:remaining]} ml to go") +
72
+ "\nStreak: #{log.streak(day)} day(s)"
73
+ when "week"
74
+ r = log.week(day)
75
+ text = r.map { |d| "#{d[:date]} #{d[:total].to_s.rjust(5)} ml #{d[:total] >= log.goal ? 'goal' : ''}" }.join("\n")
76
+ text += "\nAverage: #{log.average(day)} ml/day"
77
+ when "undo"
78
+ e = log.undo(day)
79
+ save.call
80
+ r = e&.to_h
81
+ text = e ? "Removed #{e.ml} ml from #{e.date}." : "Nothing to undo."
82
+ when "set-goal"
83
+ g = Siplog.parse_amount(args.join(" "))
84
+ raise ArgumentError, "goal must be between 500 and 6000 ml" unless (500..6000).cover?(g)
85
+ log.goal = g
86
+ save.call
87
+ r = { goal: g }
88
+ text = "Daily goal set to #{g} ml."
89
+ else
90
+ raise ArgumentError, %(Unknown command "#{cmd}". Run with --help.)
91
+ end
92
+ end
93
+ out.puts(o[:json] ? JSON.pretty_generate(r) : text)
94
+ 0
95
+ rescue ArgumentError, OptionParser::ParseError, SystemCallError, JSON::ParserError => e
96
+ err.puts "Error: #{e.message}"
97
+ 2
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPromotional
4
+ module Siplog
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "json"
5
+ require_relative "siplog/version"
6
+
7
+ module GoPromotional
8
+ # Hydration helpers: daily water goal estimate, amount parsing and a day-by-day water log.
9
+ module Siplog
10
+ ML_PER_US_FL_OZ = 29.5735
11
+ ML_PER_ACTIVE_MINUTE = 12
12
+ HOT_WEATHER_ML = 500
13
+ FACTORS = { "ml" => 1, "cl" => 10, "l" => 1000, "oz" => ML_PER_US_FL_OZ }.freeze
14
+ AMOUNT = /\A\s*(\d+(?:\.\d+)?)\s*(ml|cl|l|oz)?\s*\z/i
15
+
16
+ module_function
17
+
18
+ def round_half_up(x)
19
+ (x + 0.5).floor
20
+ end
21
+
22
+ def iso(value)
23
+ return Date.today.iso8601 if value.nil?
24
+ return value.iso8601 if value.is_a?(Date)
25
+ s = value.to_s.strip
26
+ raise ArgumentError, %(Invalid date "#{value}" (expected YYYY-MM-DD)) unless s.match?(/\A\d{4}-\d{2}-\d{2}\z/)
27
+ Date.iso8601(s).iso8601
28
+ rescue Date::Error
29
+ raise ArgumentError, %(Invalid date "#{value}" (expected YYYY-MM-DD))
30
+ end
31
+
32
+ # Rough daily water goal in ml (a general guide, not medical advice):
33
+ # weight x ml_per_kg + active minutes x 12 ml (+500 ml in hot weather), to the nearest 50 ml.
34
+ def daily_goal(weight_kg:, activity_minutes: 0, hot: false, ml_per_kg: 33)
35
+ raise ArgumentError, "weight_kg must be between 0 and 400" unless weight_kg.is_a?(Numeric) && weight_kg.positive? && weight_kg <= 400
36
+ raise ArgumentError, "activity_minutes must be >= 0" if activity_minutes.negative?
37
+ raw = weight_kg * ml_per_kg + activity_minutes * ML_PER_ACTIVE_MINUTE + (hot ? HOT_WEATHER_ML : 0)
38
+ (raw / 50.0 + 0.5).floor * 50
39
+ end
40
+
41
+ # Parse 250, "500ml", "0.5l", "33cl" or "16oz" into whole millilitres.
42
+ def parse_amount(value)
43
+ if value.is_a?(Numeric)
44
+ raise ArgumentError, "amount must be positive" unless value.positive?
45
+ return round_half_up(value)
46
+ end
47
+ m = AMOUNT.match(value.to_s)
48
+ raise ArgumentError, %(Cannot read amount "#{value}" (try 250, 500ml, 0.5l, 33cl or 16oz)) unless m
49
+ ml = round_half_up(m[1].to_f * FACTORS[(m[2] || "ml").downcase])
50
+ raise ArgumentError, "amount must be positive" unless ml.positive?
51
+ ml
52
+ end
53
+
54
+ Entry = Struct.new(:date, :ml) do
55
+ def to_h = { "date" => date, "ml" => ml }
56
+ end
57
+
58
+ # A day-by-day water log. The JSON file format ({goal, entries:[{date, ml}]}) is shared with the
59
+ # npm, PyPI and crates.io versions.
60
+ class HydrationLog
61
+ attr_accessor :goal
62
+ attr_reader :entries
63
+
64
+ def initialize(goal: 2000, entries: [])
65
+ @goal = goal
66
+ @entries = entries.map { |e| e.is_a?(Entry) ? e : Entry.new(Siplog.iso(e["date"] || e[:date]), Siplog.parse_amount(e["ml"] || e[:ml])) }
67
+ end
68
+
69
+ def self.from_json(data)
70
+ d = data.is_a?(String) ? JSON.parse(data) : (data || {})
71
+ new(goal: d.fetch("goal", 2000), entries: d.fetch("entries", []))
72
+ end
73
+
74
+ def to_h = { "goal" => goal, "entries" => entries.map(&:to_h) }
75
+ def to_json(*_args) = JSON.pretty_generate(to_h)
76
+
77
+ def add(amount, day = nil)
78
+ e = Entry.new(Siplog.iso(day), Siplog.parse_amount(amount))
79
+ @entries << e
80
+ e
81
+ end
82
+
83
+ # Remove the most recent entry for a day.
84
+ def undo(day = nil)
85
+ d = Siplog.iso(day)
86
+ i = @entries.rindex { |e| e.date == d }
87
+ i && @entries.delete_at(i)
88
+ end
89
+
90
+ def clear_day(day = nil)
91
+ d = Siplog.iso(day)
92
+ before = @entries.size
93
+ @entries.reject! { |e| e.date == d }
94
+ before - @entries.size
95
+ end
96
+
97
+ def total(day = nil)
98
+ d = Siplog.iso(day)
99
+ @entries.sum { |e| e.date == d ? e.ml : 0 }
100
+ end
101
+
102
+ def progress(day = nil, goal: nil)
103
+ g = goal || @goal
104
+ d = Siplog.iso(day)
105
+ t = total(d)
106
+ { date: d, total: t, goal: g, percent: g.positive? ? (t * 100.0 / g + 0.5).floor : 0,
107
+ remaining: [0, g - t].max, reached: t >= g }
108
+ end
109
+
110
+ # Totals for the 7 days ending on end_day, newest first.
111
+ def week(end_day = nil)
112
+ e = Date.iso8601(Siplog.iso(end_day))
113
+ (0..6).map { |i| { date: (e - i).iso8601, total: total(e - i) } }
114
+ end
115
+
116
+ # Consecutive days meeting the goal, ending today (or yesterday if today is not done yet).
117
+ def streak(end_day = nil, goal: nil)
118
+ g = goal || @goal
119
+ d = Date.iso8601(Siplog.iso(end_day))
120
+ d -= 1 if total(d) < g
121
+ n = 0
122
+ while n < 3660 && total(d) >= g
123
+ n += 1
124
+ d -= 1
125
+ end
126
+ n
127
+ end
128
+
129
+ def average(end_day = nil, days: 7)
130
+ e = Date.iso8601(Siplog.iso(end_day))
131
+ Siplog.round_half_up((0...days).sum { |i| total(e - i) } / days.to_f)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1 @@
1
+ require_relative "gopromotional/siplog"
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gopromotional-siplog
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - GoPromotional
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'A small hydration tracker: estimate a daily water goal from weight,
13
+ exercise and weather, log drinks in ml, cl, litres or US fl oz, and see today''s
14
+ progress, the last 7 days, your average and your streak. The log is a plain JSON
15
+ file.'
16
+ executables:
17
+ - gopromotional-siplog
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - exe/gopromotional-siplog
24
+ - lib/gopromotional-siplog.rb
25
+ - lib/gopromotional/siplog.rb
26
+ - lib/gopromotional/siplog/cli.rb
27
+ - lib/gopromotional/siplog/version.rb
28
+ homepage: https://www.gopromotional.co.uk/branded-products/branded-water-bottles
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ homepage_uri: https://www.gopromotional.co.uk/branded-products/branded-water-bottles
33
+ source_code_uri: https://github.com/nbkdevs/GoPromotional/tree/main/packages/water-bottles/rubygems
34
+ bug_tracker_uri: https://github.com/nbkdevs/GoPromotional/issues
35
+ rubygems_mfa_required: 'true'
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '3.0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.6.7
51
+ specification_version: 4
52
+ summary: Daily water goal estimate and a simple hydration log with streaks.
53
+ test_files: []