gopromotional-event-planner 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: 669cde9f18ffe0247e676e6d896f66ab9a9912e078ee926eb4079158ffe5a33d
4
+ data.tar.gz: e2d25654ae355a5fad56fd3684abbddcea548d49c017ab46444f2f3a720aa3b1
5
+ SHA512:
6
+ metadata.gz: 670fa9c1725886ad2b9d571cf0442e45feeada7a8e12e41905eeee483a92ead68d02aae905d05b189a427b5ac6b45c63a94b6d3a54da62ef1bb50c5a45bb49c7
7
+ data.tar.gz: c3deaa384b0be635f4428603315420b2ffb3a5999a73c1297cd6f5254f1fbf7d99b52d7abcc60978a117596767bbdf9c6ff14b9383bf4d4f3100031d27918e7a
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,59 @@
1
+ # gopromotional-event-planner
2
+
3
+ Plan an event or trade show from the command line or your code: a countdown, a 12-step planning timeline with due dates, an estimate of how many giveaway items to order, and the latest order date for printed merchandise in working days.
4
+
5
+ - **Countdown** to the event in days, and in weeks + days.
6
+ - **Planning timeline**: 12 tasks from 8 weeks out to the post-event follow-up, each with a due date, days left and an overdue flag.
7
+ - **Giveaway quantity**: attendees × days × uptake × items per person, plus a buffer, rounded up.
8
+ - **Order-by date**: production + delivery + safety margin counted back in working days (Mon–Fri).
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ gem install gopromotional-event-planner
14
+ # or in a Gemfile:
15
+ gem "gopromotional-event-planner"
16
+ ```
17
+
18
+ ## Command line
19
+
20
+ ```console
21
+ $ gopromotional-event-planner countdown 2026-12-03 --today 2026-10-19
22
+ 45 days to go (6 weeks 3 days) until 2026-12-03.
23
+
24
+ $ gopromotional-event-planner giveaways --attendees 500 --days 2 --uptake 0.6
25
+ Order 660 items (600 expected + 60 buffer).
26
+
27
+ $ gopromotional-event-planner order-by 2026-12-03 --today 2026-10-19
28
+ Order by 2026-11-12 (15 working days before the event), 24 days from today.
29
+
30
+ $ gopromotional-event-planner timeline 2026-12-03 --today 2026-10-19
31
+ ! 2026-10-08 Confirm budget and attendee estimate (overdue)
32
+ ! 2026-10-15 Book the venue (overdue)
33
+ 2026-10-22 Order branded merchandise and giveaways
34
+ 2026-10-29 Send invitations or open registration
35
+ 2026-11-05 Confirm speakers and agenda
36
+ ...
37
+ ```
38
+
39
+ Every command accepts `--json` for machine-readable output. Run `gopromotional-event-planner --help` for all options.
40
+
41
+ ## Library
42
+
43
+ ```ruby
44
+ require "gopromotional/event_planner"
45
+ EP = GoPromotional::EventPlanner
46
+
47
+ EP.countdown("2026-12-03", today: "2026-10-19")[:days] # => 45
48
+ EP.giveaway_quantity(attendees: 500, days: 2) # => {base: 600, buffer: 60, total: 660}
49
+ EP.order_by_date("2026-12-03", today: "2026-10-19")[:order_by] # => "2026-11-12"
50
+ EP.timeline("2026-12-03", today: "2026-10-19").select { |r| r[:overdue] }
51
+ ```
52
+
53
+ Pure Ruby, no runtime dependencies, Ruby 3.0+. `require "gopromotional-event-planner"` also works.
54
+
55
+ ## Links
56
+
57
+ - Website: [https://www.gopromotional.co.uk/](https://www.gopromotional.co.uk/)
58
+ - Source code: [https://github.com/nbkdevs/GoPromotional](https://github.com/nbkdevs/GoPromotional) (`packages/homepage/rubygems`)
59
+ - Licence: MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/gopromotional/event_planner/cli"
5
+ exit GoPromotional::EventPlanner::CLI.run(ARGV)
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require_relative "../event_planner"
6
+
7
+ module GoPromotional
8
+ module EventPlanner
9
+ # Command line interface: countdown | timeline | giveaways | order-by
10
+ module CLI
11
+ USAGE = <<~TXT
12
+ Usage: gopromotional-event-planner <command> [options]
13
+
14
+ Commands:
15
+ countdown <date> days until the event
16
+ timeline <date> planning checklist with due dates
17
+ giveaways --attendees N [--days N] [--uptake 0.6] [--per-person 1] [--buffer 0.1]
18
+ order-by <date> [--production 10] [--delivery 3] [--safety 2]
19
+
20
+ Options: --json, --today YYYY-MM-DD, -v/--version, -h/--help
21
+ TXT
22
+
23
+ module_function
24
+
25
+ def run(argv, out: $stdout, err: $stderr)
26
+ o = { json: false, today: Date.today.iso8601, days: 1.0, uptake: 0.6, per_person: 1.0, buffer: 0.1,
27
+ production: 10, delivery: 3, safety: 2 }
28
+ parser = OptionParser.new do |p|
29
+ p.banner = USAGE
30
+ p.on("--json") { o[:json] = true }
31
+ p.on("--today DATE") { |v| o[:today] = v }
32
+ p.on("--attendees N", Float) { |v| o[:attendees] = v }
33
+ p.on("--days N", Float) { |v| o[:days] = v }
34
+ p.on("--uptake N", Float) { |v| o[:uptake] = v }
35
+ p.on("--per-person N", Float) { |v| o[:per_person] = v }
36
+ p.on("--buffer N", Float) { |v| o[:buffer] = v }
37
+ p.on("--production N", Integer) { |v| o[:production] = v }
38
+ p.on("--delivery N", Integer) { |v| o[:delivery] = v }
39
+ p.on("--safety N", Integer) { |v| o[:safety] = v }
40
+ p.on("-v", "--version") { out.puts VERSION; return 0 }
41
+ p.on("-h", "--help") { out.puts USAGE; return 0 }
42
+ end
43
+ args = parser.parse(argv)
44
+ cmd = args.shift
45
+ need_date = -> { args.first or raise ArgumentError, "#{cmd} needs an event date (YYYY-MM-DD)" }
46
+ case cmd
47
+ when "countdown"
48
+ r = EventPlanner.countdown(need_date.call, today: o[:today])
49
+ text = case r[:status]
50
+ when "today" then "#{r[:event_date]} is today."
51
+ when "upcoming" then "#{r[:days]} days to go (#{r[:weeks]} weeks #{r[:remainder_days]} days) until #{r[:event_date]}."
52
+ else "#{r[:event_date]} was #{-r[:days]} days ago."
53
+ end
54
+ when "timeline"
55
+ r = EventPlanner.timeline(need_date.call, today: o[:today])
56
+ text = r.map { |x| "#{x[:overdue] ? '!' : ' '} #{x[:due]} #{x[:task]}#{x[:overdue] ? ' (overdue)' : ''}" }.join("\n")
57
+ when "giveaways"
58
+ raise ArgumentError, "giveaways needs --attendees N" unless o[:attendees]
59
+ r = EventPlanner.giveaway_quantity(attendees: o[:attendees], days: o[:days], uptake: o[:uptake],
60
+ per_person: o[:per_person], buffer: o[:buffer])
61
+ text = "Order #{r[:total]} items (#{r[:base]} expected + #{r[:buffer]} buffer)."
62
+ when "order-by"
63
+ r = EventPlanner.order_by_date(need_date.call, production_days: o[:production], delivery_days: o[:delivery],
64
+ safety_days: o[:safety], today: o[:today])
65
+ text = "Order by #{r[:order_by]} (#{r[:working_days]} working days before the event)"
66
+ text += r[:late] ? " - that is #{-r[:days_from_today]} days ago, so ask about express production." : ", #{r[:days_from_today]} days from today."
67
+ when nil
68
+ out.puts USAGE
69
+ return 0
70
+ else
71
+ raise ArgumentError, %(Unknown command "#{cmd}". Run with --help.)
72
+ end
73
+ out.puts(o[:json] ? JSON.pretty_generate(r) : text)
74
+ 0
75
+ rescue ArgumentError, OptionParser::ParseError => e
76
+ err.puts "Error: #{e.message}"
77
+ 2
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoPromotional
4
+ module EventPlanner
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require_relative "event_planner/version"
5
+
6
+ module GoPromotional
7
+ # Event planning helpers: countdown, planning timeline, giveaway quantities and order-by dates.
8
+ module EventPlanner
9
+ TimelineItem = Struct.new(:offset_days, :category, :task, keyword_init: true)
10
+
11
+ TIMELINE_TEMPLATE = [
12
+ [56, "planning", "Confirm budget and attendee estimate"],
13
+ [49, "venue", "Book the venue"],
14
+ [42, "merchandise", "Order branded merchandise and giveaways"],
15
+ [35, "comms", "Send invitations or open registration"],
16
+ [28, "programme", "Confirm speakers and agenda"],
17
+ [21, "venue", "Arrange catering"],
18
+ [14, "merchandise", "Approve print proofs and signage"],
19
+ [7, "comms", "Send a reminder to attendees"],
20
+ [3, "logistics", "Confirm deliveries and the staffing rota"],
21
+ [1, "logistics", "Pack the event kit and giveaways"],
22
+ [0, "logistics", "Event day: set up the stand and registration"],
23
+ [-2, "comms", "Send thank-you and follow-up messages"]
24
+ ].map { |o, c, t| TimelineItem.new(offset_days: o, category: c, task: t).freeze }.freeze
25
+
26
+ module_function
27
+
28
+ # Accept a Date or an ISO YYYY-MM-DD string.
29
+ def to_date(value)
30
+ return value if value.is_a?(Date)
31
+ s = value.to_s.strip
32
+ raise ArgumentError, %(Invalid date "#{value}" (expected YYYY-MM-DD)) unless s.match?(/\A\d{4}-\d{2}-\d{2}\z/)
33
+ begin
34
+ Date.iso8601(s)
35
+ rescue Date::Error
36
+ raise ArgumentError, %(Invalid calendar date "#{s}")
37
+ end
38
+ end
39
+
40
+ # Days until an event, split into weeks and days.
41
+ def countdown(event_date, today: Date.today)
42
+ ev = to_date(event_date)
43
+ now = to_date(today)
44
+ days = (ev - now).to_i
45
+ {
46
+ event_date: ev.iso8601, today: now.iso8601, days: days,
47
+ weeks: days.abs / 7, remainder_days: days.abs % 7,
48
+ status: days.positive? ? "upcoming" : (days.zero? ? "today" : "past")
49
+ }
50
+ end
51
+
52
+ # Planning tasks with due dates, sorted by date. With today: each row gets :days_left and :overdue.
53
+ def timeline(event_date, today: nil, template: TIMELINE_TEMPLATE)
54
+ ev = to_date(event_date)
55
+ now = today && to_date(today)
56
+ template.map do |item|
57
+ due = ev - item.offset_days
58
+ row = { task: item.task, category: item.category, offset_days: item.offset_days, due: due.iso8601 }
59
+ if now
60
+ row[:days_left] = (due - now).to_i
61
+ row[:overdue] = due < now
62
+ end
63
+ row
64
+ end.sort_by { |r| r[:due] }
65
+ end
66
+
67
+ def ceil_clean(x)
68
+ ((x * 1e6).round / 1e6).ceil
69
+ end
70
+
71
+ # Giveaway items: attendees x days x uptake x per_person, plus a buffer (both rounded up).
72
+ def giveaway_quantity(attendees:, days: 1, uptake: 0.6, per_person: 1, buffer: 0.1)
73
+ { attendees: attendees, days: days, uptake: uptake, per_person: per_person, buffer: buffer }.each do |name, v|
74
+ unless v.is_a?(Numeric) && v.finite? && v >= 0
75
+ raise ArgumentError, "#{name} must be a non-negative number"
76
+ end
77
+ end
78
+ raise ArgumentError, "uptake must be between 0 and 1" if uptake > 1
79
+ base = ceil_clean(attendees * days * uptake * per_person)
80
+ extra = ceil_clean(base * buffer)
81
+ { base: base, buffer: extra, total: base + extra }
82
+ end
83
+
84
+ # Step back n working days (Monday to Friday).
85
+ def subtract_working_days(day, n)
86
+ d = to_date(day)
87
+ left = n
88
+ while left.positive?
89
+ d -= 1
90
+ left -= 1 if d.cwday <= 5
91
+ end
92
+ d
93
+ end
94
+
95
+ # Latest order date for printed merchandise: production + delivery + safety, in working days.
96
+ def order_by_date(event_date, production_days: 10, delivery_days: 3, safety_days: 2, today: nil)
97
+ { production_days: production_days, delivery_days: delivery_days, safety_days: safety_days }.each do |name, v|
98
+ raise ArgumentError, "#{name} must be a non-negative integer" unless v.is_a?(Integer) && v >= 0
99
+ end
100
+ working = production_days + delivery_days + safety_days
101
+ by = subtract_working_days(event_date, working)
102
+ out = { order_by: by.iso8601, working_days: working }
103
+ if today
104
+ out[:days_from_today] = (by - to_date(today)).to_i
105
+ out[:late] = out[:days_from_today].negative?
106
+ end
107
+ out
108
+ end
109
+ end
110
+ end
@@ -0,0 +1 @@
1
+ require_relative "gopromotional/event_planner"
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gopromotional-event-planner
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: 'Plan an event or trade show from the command line or your code: a countdown,
13
+ a 12-step planning timeline with due dates, an estimate of how many giveaway items
14
+ to order, and the latest order date for printed merchandise in working days.'
15
+ executables:
16
+ - gopromotional-event-planner
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - exe/gopromotional-event-planner
23
+ - lib/gopromotional-event-planner.rb
24
+ - lib/gopromotional/event_planner.rb
25
+ - lib/gopromotional/event_planner/cli.rb
26
+ - lib/gopromotional/event_planner/version.rb
27
+ homepage: https://www.gopromotional.co.uk/
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ homepage_uri: https://www.gopromotional.co.uk/
32
+ source_code_uri: https://github.com/nbkdevs/GoPromotional/tree/main/packages/homepage/rubygems
33
+ bug_tracker_uri: https://github.com/nbkdevs/GoPromotional/issues
34
+ rubygems_mfa_required: 'true'
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '3.0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.6.7
50
+ specification_version: 4
51
+ summary: Event countdown, planning timeline, giveaway quantities and print order-by
52
+ dates.
53
+ test_files: []