dsu 0.1.0.alpha.5 → 1.1.0.alpha.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile.lock +6 -15
  5. data/README.md +33 -47
  6. data/lib/dsu/base_cli.rb +13 -6
  7. data/lib/dsu/cli.rb +46 -55
  8. data/lib/dsu/command_services/add_entry_service.rb +21 -21
  9. data/lib/dsu/core/ruby/not_today.rb +11 -0
  10. data/lib/dsu/models/entry.rb +32 -21
  11. data/lib/dsu/models/entry_group.rb +41 -105
  12. data/lib/dsu/services/configuration_loader_service.rb +19 -2
  13. data/lib/dsu/services/entry_group_editor_service.rb +37 -89
  14. data/lib/dsu/services/stdout_redirector_service.rb +27 -0
  15. data/lib/dsu/subcommands/list.rb +83 -15
  16. data/lib/dsu/support/colorable.rb +1 -0
  17. data/lib/dsu/support/command_options/dsu_times.rb +33 -0
  18. data/lib/dsu/support/command_options/time.rb +77 -0
  19. data/lib/dsu/support/command_options/time_mneumonic.rb +127 -0
  20. data/lib/dsu/support/command_options/time_mneumonics.rb +15 -0
  21. data/lib/dsu/support/configurable.rb +15 -0
  22. data/lib/dsu/support/configuration.rb +13 -1
  23. data/lib/dsu/support/entry_group_fileable.rb +31 -6
  24. data/lib/dsu/support/entry_group_loadable.rb +13 -16
  25. data/lib/dsu/support/entry_group_viewable.rb +26 -8
  26. data/lib/dsu/support/times_sortable.rb +1 -3
  27. data/lib/dsu/validators/description_validator.rb +38 -0
  28. data/lib/dsu/validators/entries_validator.rb +43 -32
  29. data/lib/dsu/validators/time_validator.rb +11 -20
  30. data/lib/dsu/version.rb +1 -1
  31. data/lib/dsu/views/edited_entries/shared/errors.rb +39 -0
  32. data/lib/dsu/views/entry_group/edit.rb +89 -39
  33. data/lib/dsu/views/entry_group/show.rb +10 -4
  34. data/lib/dsu/views/shared/messages.rb +56 -0
  35. data/lib/dsu.rb +8 -2
  36. metadata +24 -12
  37. data/lib/dsu/support/commander/command.rb +0 -130
  38. data/lib/dsu/support/commander/command_help.rb +0 -62
  39. data/lib/dsu/support/commander/subcommand.rb +0 -45
  40. data/lib/dsu/support/interactive/cli.rb +0 -161
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'deco_lite'
3
+ require 'active_model'
4
4
  require_relative '../services/entry_group_editor_service'
5
5
  require_relative '../services/entry_group_deleter_service'
6
6
  require_relative '../services/entry_group_reader_service'
@@ -13,30 +13,28 @@ require_relative 'entry'
13
13
 
14
14
  module Dsu
15
15
  module Models
16
- class EntryGroup < DecoLite::Model
16
+ # This class represents a group of entries for a given day. IOW,
17
+ # things someone might want to share at their daily standup (DSU).
18
+ class EntryGroup
19
+ include ActiveModel::Model
17
20
  extend Support::EntryGroupLoadable
18
21
  include Support::TimeFormatable
19
22
 
20
- validates_with Validators::EntriesValidator, fields: [:entries]
21
- validates_with Validators::TimeValidator, fields: [:time]
23
+ attr_accessor :time
24
+ attr_reader :entries
25
+
26
+ validates_with Validators::EntriesValidator
27
+ validates_with Validators::TimeValidator
22
28
 
23
29
  def initialize(time: nil, entries: [])
24
30
  raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
25
- raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array) || entries.nil?
26
-
27
- time ||= Time.now
28
- time = time.localtime if time.utc?
29
31
 
30
- entries ||= []
31
-
32
- super(hash: {
33
- time: time,
34
- entries: entries
35
- })
32
+ @time = ensure_local_time(time)
33
+ self.entries = entries || []
36
34
  end
37
35
 
38
36
  class << self
39
- def delete(time:, options: {})
37
+ def delete!(time:, options: {})
40
38
  Services::EntryGroupDeleterService.new(time: time, options: options).call
41
39
  end
42
40
 
@@ -46,127 +44,65 @@ module Dsu
46
44
  # return new(time: time) unless exists?(time: time)
47
45
 
48
46
  load(time: time).tap do |entry_group|
49
- entry_group.edit(options: options)
47
+ Services::EntryGroupEditorService.new(entry_group: entry_group, options: options).call
50
48
  end
51
49
  end
52
50
 
53
51
  def exists?(time:)
54
52
  Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
55
53
  end
54
+ end
56
55
 
57
- # Loads the EntryGroup from the file system and returns an
58
- # instantiated EntryGroup object.
59
- def load(time: nil)
60
- new(**hydrated_entry_group_hash_for(time: time))
61
- end
62
-
63
- # This function returns a hash whose :time and :entries
64
- # key values are hydrated with instantiated Time and Entry
65
- # objects.
66
- def hydrated_entry_group_hash_for(time:)
67
- entry_group_hash = entry_group_hash_for(time: time)
68
- hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
69
- end
56
+ def valid_unique_entries
57
+ entries&.select(&:valid?)&.uniq(&:description)
58
+ end
70
59
 
71
- def unique?(entry:)
60
+ def clone
61
+ clone = super
72
62
 
73
- end
63
+ clone.entries = clone.entries.map(&:clone)
64
+ clone
74
65
  end
75
66
 
76
- def required_fields
77
- %i[time entries]
78
- end
67
+ def entries=(entries)
68
+ entries ||= []
79
69
 
80
- def edit(options: {})
81
- Services::EntryGroupEditorService.new(entry_group: self, options: options).call
82
- self
70
+ raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array)
71
+ raise ArgumentError, 'entries contains the wrong object type' unless entries.all?(Entry)
72
+
73
+ @entries = entries.map(&:clone)
83
74
  end
84
75
 
85
76
  # Deletes the entry group file from the file system.
86
- def delete
87
- self.class.delete(time: time)
77
+ def delete!
78
+ self.class.delete!(time: time)
79
+ self.entries = []
88
80
  self
89
81
  end
90
82
 
91
- def entries?
92
- entries.any?
83
+ def time_formatted
84
+ formatted_time(time: time)
93
85
  end
94
86
 
95
87
  def save!
88
+ delete! and return if entries.empty?
89
+
96
90
  validate!
97
91
  Services::EntryGroupWriterService.new(entry_group: self).call
92
+ self
98
93
  end
99
94
 
100
95
  def to_h
101
- super.tap do |hash|
102
- hash[:entries] = hash[:entries].dup
103
- hash[:entries].each_with_index do |entry, index|
104
- hash[:entries][index] = entry.to_h
105
- end
106
- end
107
- end
108
-
109
- def check_unique(sha_or_editor_command:, description:)
110
- raise ArgumentError, 'sha_or_editor_command is nil' if sha_or_editor_command.nil?
111
- raise ArgumentError, 'description is nil' if description.nil?
112
- raise ArgumentError, 'sha_or_editor_command is the wrong object type' unless sha_or_editor_command.is_a?(String)
113
- raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
114
-
115
- if entries.blank?
116
- entry_unique_hash = entry_unique_hash_for(uuid_unique: true, description_unique: true)
117
- return entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
118
- end
119
-
120
- entry_hash = entries.each_with_object({}) do |entry_group_entry, hash|
121
- hash[entry_group_entry.uuid] = entry_group_entry.description
122
- end
123
-
124
- # It is possible that sha_or_editor_command may have an editor command (e.g. +|a|add). If this
125
- # is the case, just treat it as unique because when the entry is added, it will get a unique uuid.
126
- uuid_unique = !sha_or_editor_command.match?(Entry::ENTRY_UUID_REGEX) || !entry_hash.key?(sha_or_editor_command)
127
- entry_unique_hash = entry_unique_hash_for(
128
- uuid: sha_or_editor_command,
129
- uuid_unique: uuid_unique,
130
- description: description,
131
- description_unique: !entry_hash.value?(description)
132
- )
133
- entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
134
- end
135
-
136
- def entry_unique_hash_for(uuid_unique:, description_unique:, uuid: nil, description: nil)
137
96
  {
138
- uuid: uuid,
139
- uuid_unique: uuid_unique,
140
- description: description,
141
- description_unique: description_unique,
142
- formatted_time: Support::TimeFormatable.formatted_time(time: time)
97
+ time: time.dup,
98
+ entries: entries.map(&:to_h)
143
99
  }
144
100
  end
145
101
 
146
- def entry_unique_struct_from(entry_unique_hash:)
147
- Struct.new(*entry_unique_hash.keys, keyword_init: true) do
148
- def unique?
149
- uuid_unique? && description_unique?
150
- end
151
-
152
- def uuid_unique?
153
- uuid_unique
154
- end
155
-
156
- def description_unique?
157
- description_unique
158
- end
159
-
160
- def messages
161
- return [] if unique?
102
+ private
162
103
 
163
- short_description = Models::Entry.short_description(string: description)
164
-
165
- messages = []
166
- messages << "#uuid is not unique: \"#{uuid} #{short_description}\"" unless uuid_unique?
167
- messages << "#description is not unique: \"#{uuid} #{short_description}\""
168
- end
169
- end.new(**entry_unique_hash)
104
+ def ensure_local_time(time)
105
+ time.nil? ? Time.now : time.dup.localtime
170
106
  end
171
107
  end
172
108
  end
@@ -6,6 +6,7 @@ require_relative '../support/configuration'
6
6
 
7
7
  module Dsu
8
8
  module Services
9
+ # This class loads an entry group file.
9
10
  class ConfigurationLoaderService
10
11
  include Dsu::Support::Configuration
11
12
 
@@ -29,9 +30,25 @@ module Dsu
29
30
  attr_reader :default_options
30
31
 
31
32
  def config_options
32
- return Support::Configuration::DEFAULT_DSU_OPTIONS unless config_file?
33
+ return default_config unless config_file?
33
34
 
34
- @config_options ||= YAML.safe_load(ERB.new(File.read(config_file)).result)
35
+ @config_options ||= begin
36
+ loaded_config = YAML.safe_load(ERB.new(File.read(config_file)).result)
37
+ loaded_config = update_and_write_config_file!(loaded_config) unless loaded_config.keys == default_config.keys
38
+ loaded_config
39
+ end
40
+ end
41
+
42
+ def update_and_write_config_file!(loaded_config)
43
+ loaded_config = default_config.merge(loaded_config)
44
+ # TODO: Make this into a configuration writer service.
45
+ # TODO: Test this
46
+ File.write(config_file, loaded_config.to_yaml)
47
+ loaded_config
48
+ end
49
+
50
+ def default_config
51
+ Support::Configuration::DEFAULT_DSU_OPTIONS
35
52
  end
36
53
  end
37
54
  end
@@ -2,14 +2,18 @@
2
2
 
3
3
  require_relative '../models/entry'
4
4
  require_relative '../support/colorable'
5
+ require_relative '../support/configurable'
5
6
  require_relative '../support/say'
6
7
  require_relative '../support/time_formatable'
7
- require_relative 'configuration_loader_service'
8
+ require_relative '../views/edited_entries/shared/errors'
9
+ require_relative '../views/shared/messages'
10
+ require_relative 'stdout_redirector_service'
8
11
 
9
12
  module Dsu
10
13
  module Services
11
14
  class EntryGroupEditorService
12
15
  include Support::Colorable
16
+ include Support::Configurable
13
17
  include Support::Say
14
18
  include Support::TimeFormatable
15
19
 
@@ -24,7 +28,7 @@ module Dsu
24
28
 
25
29
  def call
26
30
  edit_view = render_edit_view
27
- edit!(edit_view: edit_view)
31
+ edit edit_view
28
32
  # NOTE: Return the original entry group object as any permanent changes
29
33
  # will have been applied to it.
30
34
  entry_group
@@ -38,110 +42,54 @@ module Dsu
38
42
  # and edit it. The edits will be used to update the entry group.
39
43
  def render_edit_view
40
44
  say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
41
- capture_stdxxx { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
45
+ StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
42
46
  end
43
47
 
44
- # Writes the temporary file contents to disk and opens it in the editor.
45
- def edit!(edit_view:)
48
+ # Writes the temporary file contents to disk and opens it in the editor
49
+ # for editing. It then copies the changes to the entry group and writes
50
+ # the changes to the entry group file.
51
+ def edit(edit_view)
52
+ entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
53
+
46
54
  Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
47
- unless Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
48
- say "Failed to open temporary file in editor '#{configuration[:editor]}';" \
55
+ if Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
56
+ Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
57
+ next unless process_description?(editor_line)
58
+
59
+ entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
60
+ end
61
+
62
+ process_entry_group!(entry_group_with_edits)
63
+ else
64
+ say "Failed to open temporary file in editor '#{configuration[:editor]}'; " \
49
65
  "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
50
66
  say 'Either set the EDITOR environment variable ' \
51
67
  'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
52
- say 'Run `$ dsu help config` for more information.', ERROR
53
-
54
- system('dsu help config')
55
-
56
- return # rubocop:disable Lint/NonLocalExitFromIterator: This is not an iterator.
68
+ say 'Run `$ dsu help config` for more information:', ERROR
57
69
  end
58
-
59
- update_entry_group!(tmp_file_path: tmp_file_path)
60
70
  end
61
71
  end
62
72
 
63
- # TODO: Clean this up
64
- def update_entry_group!(tmp_file_path:)
65
- errors = []
66
- entry_group.entries = entries = []
67
- Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |tmp_file_line|
68
- next if comment_or_empty?(tmp_file_line: tmp_file_line)
69
-
70
- entry_info = editor_entry_info_from(tmp_file_line: tmp_file_line)
71
- next if entry_info.empty?
72
- next if delete_entry_cmd?(sha: entry_info[:sha])
73
- next unless add_entry_cmd?(sha: entry_info[:sha]) || sha?(sha: entry_info[:sha])
74
-
75
- entry_info[:sha_or_editor_command] = entry_info[:sha]
76
- entry_info[:sha] = nil if add_entry_cmd?(sha: entry_info[:sha])
77
-
78
- entry = Models::Entry.new(uuid: entry_info[:sha], description: entry_info[:description])
79
- entry_group.check_unique(sha_or_editor_command: entry_info[:sha_or_editor_command],
80
- description: entry_info[:description]).tap do |status|
81
- entries << entry and next if status.unique?
82
-
83
- errors << status.messages
84
- end
73
+ def process_entry_group!(entry_group_with_edits)
74
+ if entry_group_with_edits.entries.empty?
75
+ entry_group.delete!
76
+ return
85
77
  end
86
78
 
87
- # Display any errors encountered.
88
- if errors.any?
89
- say 'Error: one or more entry values were not unique within the entry group entries:', ERROR
90
- errors.flatten.each { |message| say "Error: #{message}", ERROR }
79
+ if entry_group_with_edits.invalid?
80
+ header = 'The following ERRORS were encountered; these changes were not saved:'
81
+ messages = entry_group_with_edits.errors.full_messages
82
+ Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
91
83
  end
92
84
 
93
- # Save or delete any entries.
94
- entry_group.entries = entries
95
- entry_group.delete and return unless entry_group.entries?
96
-
85
+ # Make sure we're saving only valid, unique entries.
86
+ entry_group.entries = entry_group_with_edits.valid_unique_entries
97
87
  entry_group.save!
98
88
  end
99
89
 
100
- def sha?(sha:)
101
- sha.match?(Models::Entry::ENTRY_UUID_REGEX)
102
- end
103
-
104
- def delete_entry_cmd?(sha:)
105
- %w[- d delete].include?(sha)
106
- end
107
-
108
- def add_entry_cmd?(sha:)
109
- %w[+ a add].include?(sha)
110
- end
111
-
112
- def comment_or_empty?(tmp_file_line:)
113
- ['#', nil].include? tmp_file_line[0]
114
- end
115
-
116
- def editor_entry_info_from(tmp_file_line:)
117
- match_data = tmp_file_line.match(/(\S+)\s(.+)/)
118
- {
119
- sha: match_data[1]&.strip,
120
- description: match_data[2]&.strip
121
- }
122
- rescue StandardError
123
- {}
124
- end
125
-
126
- # TODO: Add this to a module.
127
- # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
128
- def capture_stdxxx
129
- # The output stream must be an IO-like object. In this case we capture it in
130
- # an in-memory IO object so we can return the string value. You can assign any
131
- # IO object here.
132
- string_io = StringIO.new
133
- prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
134
- prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
135
- yield
136
- string_io.string
137
- ensure
138
- # Restore the previous value of stderr and stdout (typically equal to STDERR).
139
- $stdout = prev_stdout
140
- $stderr = prev_stderr
141
- end
142
-
143
- def configuration
144
- @configuration ||= ConfigurationLoaderService.new.call
90
+ def process_description?(description)
91
+ description = Models::Entry.clean_description(description)
92
+ !(description.blank? || description[0] == '#')
145
93
  end
146
94
  end
147
95
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Services
5
+ # This service captures $stdout, resirects it to a StringIO object,
6
+ # and returns the string value.
7
+ # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
8
+ module StdoutRedirectorService
9
+ class << self
10
+ def call
11
+ raise ArgumentError, 'no block was provided' unless block_given?
12
+
13
+ # The output stream must be an IO-like object. In this case we capture it in
14
+ # an in-memory IO object so we can return the string value. Any IO object can
15
+ # be used here.
16
+ string_io = StringIO.new
17
+ original_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
18
+ yield
19
+ string_io.string
20
+ ensure
21
+ # Restore the original $stdout.
22
+ $stdout = original_stdout
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../base_cli'
4
+ require_relative '../support/command_options/dsu_times'
5
+ require_relative '../support/time_formatable'
4
6
 
5
7
  module Dsu
6
8
  module Subcommands
7
9
  class List < Dsu::BaseCLI
10
+ include Support::CommandOptions::DsuTimes
11
+ include Support::TimeFormatable
12
+
8
13
  map %w[d] => :date
14
+ map %w[dd] => :dates
9
15
  map %w[n] => :today
10
16
  map %w[t] => :tomorrow
11
17
  map %w[y] => :yesterday
@@ -17,10 +23,8 @@ module Dsu
17
23
  LONG_DESC
18
24
  def today
19
25
  time = Time.now
20
- sorted_dsu_times_for(times: [time, time.yesterday]).each do |t|
21
- view_entry_group(time: t)
22
- puts
23
- end
26
+ times = sorted_dsu_times_for(times: [time, time.yesterday])
27
+ view_list_for(times: times)
24
28
  end
25
29
 
26
30
  desc 'tomorrow, t',
@@ -30,10 +34,8 @@ module Dsu
30
34
  LONG_DESC
31
35
  def tomorrow
32
36
  time = Time.now
33
- sorted_dsu_times_for(times: [time.tomorrow, time]).each do |t|
34
- view_entry_group(time: t)
35
- puts
36
- end
37
+ times = sorted_dsu_times_for(times: [time.tomorrow, time])
38
+ view_list_for(times: times)
37
39
  end
38
40
 
39
41
  desc 'yesterday, y',
@@ -43,10 +45,8 @@ module Dsu
43
45
  LONG_DESC
44
46
  def yesterday
45
47
  time = Time.now
46
- sorted_dsu_times_for(times: [time.yesterday, time.yesterday.yesterday]).each do |t|
47
- view_entry_group(time: t)
48
- puts
49
- end
48
+ times = sorted_dsu_times_for(times: [time.yesterday, time.yesterday.yesterday])
49
+ view_list_for(times: times)
50
50
  end
51
51
 
52
52
  desc 'date, d DATE',
@@ -57,14 +57,82 @@ module Dsu
57
57
  LONG_DESC
58
58
  def date(date)
59
59
  time = Time.parse(date)
60
- sorted_dsu_times_for(times: [time, time.yesterday]).each do |t|
61
- view_entry_group(time: t)
62
- puts
60
+ times = sorted_dsu_times_for(times: [time, time.yesterday])
61
+ view_list_for(times: times)
62
+ rescue ArgumentError => e
63
+ say "Error: #{e.message}", ERROR
64
+ exit 1
65
+ end
66
+
67
+ desc 'dates, dd OPTIONS',
68
+ 'Displays the DSU entries for the OPTIONS provided'
69
+ long_desc <<~LONG_DESC
70
+ NAME
71
+ \x5
72
+ `dsu dates|dd OPTIONS` -- will display the DSU entries for the OPTIONS provided.
73
+
74
+ SYNOPSIS
75
+ \x5
76
+ `dsu dates|dd OPTIONS`
77
+
78
+ OPTIONS:
79
+ \x5
80
+ -f|--from DATE|MNEMONIC: ?.
81
+
82
+ \x5
83
+ -t|--to DATE|MNEMONIC: ?.
84
+
85
+ \x5
86
+ #{date_option_description}
87
+
88
+ \x5
89
+ #{mneumonic_option_description}
90
+ LONG_DESC
91
+ # -f, --from FROM [DATE|MNEMONIC] (e.g. -f, --from 1/1[/yyy]|n|t|y|today|tomorrow|yesterday)
92
+ option :from, type: :string, aliases: '-f', banner: 'DATE|MNEMONIC'
93
+ # -t, --to TO [DATE|MNEMONIC] (e.g. -t, --to 1/1[/yyy]|n|t|y|today|tomorrow|yesterday)
94
+ option :to, type: :string, aliases: '-t', banner: 'DATE|MNEMONIC'
95
+
96
+ # Include dates that have no DSU entries.
97
+ option :include_all, type: :boolean, aliases: '-a'
98
+ def dates
99
+ options = configuration.merge(self.options)
100
+ times = dsu_times_from!(from_command_option: options[:from], to_command_option: options[:to])
101
+ # Note special sort here, unlike the other commands where rules for
102
+ # displaying DSU entries are applied; this is more of a list command.
103
+ times = times_sort(times: times, entries_display_order: entries_display_order)
104
+ view_entry_groups(times: times, options: options) do |total_entry_groups|
105
+ nothing_to_display_banner_for(times) if total_entry_groups.zero?
63
106
  end
64
107
  rescue ArgumentError => e
65
108
  say "Error: #{e.message}", ERROR
66
109
  exit 1
67
110
  end
111
+
112
+ private
113
+
114
+ def nothing_to_display_banner_for(entry_group_times)
115
+ entry_group_times.sort!
116
+ time_range = "#{formatted_time(time: entry_group_times.first)} " \
117
+ "through #{formatted_time(time: entry_group_times.last)}"
118
+ say "(nothing to display for #{time_range})", INFO
119
+ end
120
+
121
+ # This method will unconditionally display the FIRST and LAST entry groups
122
+ # associated with the times provided by the <times> argument. All other
123
+ # entry groups will be conditionally displayed based on the :include_all
124
+ # value in the <options> argument.
125
+ def view_list_for(times:)
126
+ options = configuration.merge(self.options)
127
+ times_first_and_last = [times.first, times.last]
128
+ times.each do |time|
129
+ view_options = options.dup
130
+ view_options[:include_all] = true if times_first_and_last.include?(time)
131
+ view_entry_group(time: time, options: view_options) do
132
+ puts
133
+ end
134
+ end
135
+ end
68
136
  end
69
137
  end
70
138
  end
@@ -6,6 +6,7 @@ module Dsu
6
6
  ABORTED = :red
7
7
  ERROR = :red
8
8
  HIGHLIGHT = :cyan
9
+ INFO = HIGHLIGHT
9
10
  SUCCESS = :green
10
11
  WARNING = :yellow
11
12
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time'
4
+ require_relative 'time_mneumonic'
5
+ require_relative 'time_mneumonics'
6
+
7
+ module Dsu
8
+ module Support
9
+ module CommandOptions
10
+ module DsuTimes
11
+ include Time
12
+ include TimeMneumonic
13
+ include TimeMneumonics
14
+
15
+ # Returns an array of Time objects. The first element is the from time. The second element is the to time.
16
+ # Both arguments are expected to be command options that are time strings, time or relative time mneumonics.
17
+ def dsu_times_from!(from_command_option:, to_command_option:)
18
+ times = begin
19
+ from_time = time_from_mneumonic(command_option: from_command_option) if time_mneumonic?(from_command_option)
20
+ from_time ||= time_from_date_string(command_option: from_command_option)
21
+
22
+ to_time = time_from_mneumonic(command_option: to_command_option) if time_mneumonic?(to_command_option)
23
+ to_time ||= time_from_date_string(command_option: to_command_option)
24
+
25
+ [from_time, to_time].sort
26
+ end
27
+
28
+ (times.min.to_date..times.max.to_date).map(&:to_time)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end