doing 2.1.39 → 2.1.40

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/commands/config.rb +43 -34
  6. data/bin/commands/done.rb +1 -18
  7. data/bin/commands/finish.rb +30 -25
  8. data/bin/commands/grep.rb +3 -14
  9. data/bin/commands/last.rb +2 -8
  10. data/bin/commands/meanwhile.rb +13 -6
  11. data/bin/commands/on.rb +3 -16
  12. data/bin/commands/recent.rb +2 -8
  13. data/bin/commands/reset.rb +24 -1
  14. data/bin/commands/select.rb +1 -1
  15. data/bin/commands/show.rb +6 -17
  16. data/bin/commands/since.rb +1 -12
  17. data/bin/commands/today.rb +2 -13
  18. data/bin/commands/view.rb +1 -1
  19. data/bin/commands/yesterday.rb +2 -13
  20. data/bin/doing +15 -8
  21. data/docs/doc/Array.html +1 -1
  22. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  23. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  24. data/docs/doc/BooleanTermParser/Query.html +1 -1
  25. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  26. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  27. data/docs/doc/BooleanTermParser.html +1 -1
  28. data/docs/doc/Doing/Color.html +166 -20
  29. data/docs/doc/Doing/Completion.html +1 -1
  30. data/docs/doc/Doing/Configuration.html +1 -1
  31. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  32. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  33. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  34. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  35. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  36. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  37. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  38. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  39. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  40. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  41. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  42. data/docs/doc/Doing/Errors.html +9 -9
  43. data/docs/doc/Doing/Hooks.html +1 -1
  44. data/docs/doc/Doing/Item.html +90 -1615
  45. data/docs/doc/Doing/Items.html +121 -5
  46. data/docs/doc/Doing/Logger.html +1 -1
  47. data/docs/doc/Doing/Note.html +1 -1
  48. data/docs/doc/Doing/Pager.html +1 -1
  49. data/docs/doc/Doing/Plugins.html +1 -1
  50. data/docs/doc/Doing/Prompt.html +2 -2
  51. data/docs/doc/Doing/Section.html +1 -1
  52. data/docs/doc/Doing/TemplateString.html +2 -2
  53. data/docs/doc/Doing/Types.html +1 -1
  54. data/docs/doc/Doing/Util/Backup.html +5 -5
  55. data/docs/doc/Doing/Util.html +1 -1
  56. data/docs/doc/Doing/WWID.html +197 -4033
  57. data/docs/doc/Doing.html +2 -2
  58. data/docs/doc/FalseClass.html +1 -1
  59. data/docs/doc/GLI/Commands/Help.html +1 -1
  60. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  61. data/docs/doc/GLI/Commands.html +1 -1
  62. data/docs/doc/GLI.html +1 -1
  63. data/docs/doc/Hash.html +1 -1
  64. data/docs/doc/Object.html +1 -1
  65. data/docs/doc/PhraseParser/Operator.html +1 -1
  66. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  67. data/docs/doc/PhraseParser/Query.html +1 -1
  68. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  69. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  70. data/docs/doc/PhraseParser/TermClause.html +1 -1
  71. data/docs/doc/PhraseParser.html +1 -1
  72. data/docs/doc/Status.html +1 -1
  73. data/docs/doc/String.html +1 -1
  74. data/docs/doc/Symbol.html +1 -1
  75. data/docs/doc/Time.html +1 -1
  76. data/docs/doc/TrueClass.html +1 -1
  77. data/docs/doc/_index.html +26 -5
  78. data/docs/doc/class_list.html +1 -1
  79. data/docs/doc/file.README.html +2 -2
  80. data/docs/doc/index.html +2 -2
  81. data/docs/doc/method_list.html +293 -773
  82. data/docs/doc/top-level-namespace.html +3 -3
  83. data/docs/index.md +1 -1
  84. data/doing.rdoc +49 -7
  85. data/lib/completion/_doing.zsh +5 -5
  86. data/lib/completion/doing.bash +8 -8
  87. data/lib/completion/doing.fish +7 -2
  88. data/lib/doing/add_options.rb +31 -1
  89. data/lib/doing/chronify/array.rb +64 -22
  90. data/lib/doing/colors.rb +77 -30
  91. data/lib/doing/completion.rb +4 -5
  92. data/lib/doing/errors.rb +51 -35
  93. data/lib/doing/hooks.rb +3 -3
  94. data/lib/doing/item/dates.rb +112 -0
  95. data/lib/doing/item/query.rb +433 -0
  96. data/lib/doing/item/state.rb +59 -0
  97. data/lib/doing/item/tags.rb +87 -0
  98. data/lib/doing/item.rb +6 -667
  99. data/lib/doing/items.rb +38 -13
  100. data/lib/doing/plugin_manager.rb +3 -3
  101. data/lib/doing/plugins/export/template_export.rb +4 -4
  102. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  103. data/lib/doing/util_backup.rb +6 -8
  104. data/lib/doing/version.rb +1 -1
  105. data/lib/doing/wwid/display.rb +399 -0
  106. data/lib/doing/wwid/editor.rb +214 -0
  107. data/lib/doing/wwid/filetools.rb +186 -0
  108. data/lib/doing/wwid/filter.rb +218 -0
  109. data/lib/doing/wwid/guess.rb +87 -0
  110. data/lib/doing/wwid/interactive.rb +385 -0
  111. data/lib/doing/wwid/modify.rb +618 -0
  112. data/lib/doing/wwid/tags.rb +54 -0
  113. data/lib/doing/wwid/timers.rb +345 -0
  114. data/lib/doing/wwid/wwidutil.rb +104 -0
  115. data/lib/doing/wwid.rb +31 -2317
  116. metadata +19 -2
data/lib/doing/colors.rb CHANGED
@@ -2,9 +2,18 @@
2
2
 
3
3
  # Cribbed from <https://github.com/flori/term-ansicolor>
4
4
  module Doing
5
- # Terminal color functions
5
+ # Terminal output color functions.
6
6
  module Color
7
- # :stopdoc:
7
+ # All available color names. Available as methods and string extensions.
8
+ #
9
+ # @example Use a color as a method. Color reset will be added to end of string.
10
+ # Color.yellow('This text is yellow') => "\e[33mThis text is yellow\e[0m"
11
+ #
12
+ # @example Use a color as a string extension. Color reset added automatically.
13
+ # 'This text is green'.green => "\e[1;32mThis text is green\e[0m"
14
+ #
15
+ # @example Send a text string as a color
16
+ # Color.send('red') => "\e[31m"
8
17
  ATTRIBUTES = [
9
18
  [:clear, 0], # String#clear is already used to empty string in Ruby 1.9
10
19
  [:reset, 0], # synonym for :clear
@@ -66,7 +75,7 @@ module Doing
66
75
  [:redacted, '0;30;40'],
67
76
  [:alert, '1;31;43'],
68
77
  [:error, '1;37;41'],
69
- [:default, '0;39']
78
+ [:default, '0;39']
70
79
  ].map(&:freeze).freeze
71
80
 
72
81
  ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
@@ -119,24 +128,60 @@ module Doing
119
128
  end
120
129
 
121
130
  class << self
122
- # Returns true, if the coloring function of this module
131
+ # Returns true if the coloring function of this module
123
132
  # is switched on, false otherwise.
124
133
  def coloring?
125
134
  @coloring
126
135
  end
127
136
 
128
- # Turns the coloring on or off globally, so you can easily do
129
- # this for example:
130
- # Doing::Color::coloring = STDOUT.isatty
131
137
  attr_writer :coloring
132
138
 
139
+ ##
140
+ ## Enables colored output
141
+ ##
142
+ ## @example Turn color on or off based on TTY
143
+ ## Doing::Color.coloring = STDOUT.isatty
133
144
  def coloring
134
145
  @coloring ||= true
135
146
  end
147
+
148
+ ##
149
+ ## Convert a template string to a colored string.
150
+ ## Colors are specified with single letters inside
151
+ ## curly braces. Uppercase changes background color.
152
+ ##
153
+ ## w: white, k: black, g: green, l: blue, y: yellow, c: cyan,
154
+ ## m: magenta, r: red, b: bold, u: underline, i: italic,
155
+ ## x: reset (remove background, color, emphasis)
156
+ ##
157
+ ## @example Convert a templated string
158
+ ## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
159
+ ##
160
+ ## @param input [String, Array] The template
161
+ ## string. If this is an array, the
162
+ ## elements will be joined with a
163
+ ## space.
164
+ ##
165
+ ## @return [String] Colorized string
166
+ ##
167
+ def template(input)
168
+ input = input.join(' ') if input.is_a? Array
169
+ fmt = input.gsub(/\{(\w+)\}/) do
170
+ Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
171
+ end
172
+
173
+ colors = { w: white, k: black, g: green, l: blue,
174
+ y: yellow, c: cyan, m: magenta, r: red,
175
+ W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
176
+ Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
177
+ b: bold, u: underline, i: italic, x: reset }
178
+
179
+ format(fmt, colors)
180
+ end
136
181
  end
137
182
 
138
183
  ATTRIBUTES.each do |c, v|
139
- eval <<-EOT
184
+ new_method = <<-EOSCRIPT
140
185
  def #{c}(string = nil)
141
186
  result = ''
142
187
  result << "\e[#{v}m" if Doing::Color.coloring?
@@ -152,33 +197,37 @@ module Doing
152
197
  result << "\e[0m" if Doing::Color.coloring?
153
198
  result
154
199
  end
155
- EOT
200
+ EOSCRIPT
201
+
202
+ module_eval(new_method)
203
+
204
+ next unless c =~ /bold/
156
205
 
157
206
  # Accept brightwhite in addition to boldwhite
158
- if c =~ /bold/
159
- eval <<-EOT
160
- def #{c.to_s.sub(/bold/, 'bright')}(string = nil)
161
- result = ''
162
- result << "\e[#{v}m" if Doing::Color.coloring?
163
- if block_given?
164
- result << yield
165
- elsif string.respond_to?(:to_str)
166
- result << string.to_str
167
- elsif respond_to?(:to_str)
168
- result << to_str
169
- else
170
- return result #only switch on
171
- end
172
- result << "\e[0m" if Doing::Color.coloring?
173
- result
207
+ new_method = <<-EOSCRIPT
208
+ def #{c.to_s.sub(/bold/, 'bright')}(string = nil)
209
+ result = ''
210
+ result << "\e[#{v}m" if Doing::Color.coloring?
211
+ if block_given?
212
+ result << yield
213
+ elsif string.respond_to?(:to_str)
214
+ result << string.to_str
215
+ elsif respond_to?(:to_str)
216
+ result << to_str
217
+ else
218
+ return result #only switch on
174
219
  end
175
- EOT
176
- end
220
+ result << "\e[0m" if Doing::Color.coloring?
221
+ result
222
+ end
223
+ EOSCRIPT
224
+
225
+ module_eval(new_method)
177
226
  end
178
227
 
179
228
  # Regular expression that is used to scan for ANSI-sequences while
180
229
  # uncoloring strings.
181
- COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/
230
+ COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/.freeze
182
231
 
183
232
  # Returns an uncolored version of the string, that is all
184
233
  # ANSI-sequences are stripped from the string.
@@ -194,8 +243,6 @@ module Doing
194
243
  end
195
244
  end
196
245
 
197
- module_function
198
-
199
246
  # Returns an array of all Doing::Color attributes as symbols.
200
247
  def attributes
201
248
  ATTRIBUTE_NAMES
@@ -2,11 +2,10 @@
2
2
 
3
3
  require 'tty-progressbar'
4
4
 
5
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'completion'))
6
- require 'string'
7
- require 'fish_completion'
8
- require 'zsh_completion'
9
- require 'bash_completion'
5
+ require_relative 'completion/string'
6
+ require_relative 'completion/fish_completion'
7
+ require_relative 'completion/zsh_completion'
8
+ require_relative 'completion/bash_completion'
10
9
 
11
10
  module Doing
12
11
  # Completion script generator
data/lib/doing/errors.rb CHANGED
@@ -2,19 +2,27 @@
2
2
 
3
3
  module Doing
4
4
  module Errors
5
- class UserCancelled < ::StandardError
6
- def initialize(msg = 'Cancelled', topic = 'Exited:')
5
+ class DoingNoTraceError < ::StandardError
6
+ def initialize(msg = nil, level: nil, topic: 'Error:', exit_code: 1)
7
+ level ||= :error
7
8
  Doing.logger.output_results
8
- Doing.logger.log_now(:warn, topic, msg)
9
- Process.exit 1
9
+ if msg
10
+ Doing.logger.log_now(level, topic, msg)
11
+ end
12
+
13
+ Process.exit exit_code
10
14
  end
11
15
  end
12
16
 
13
- class EmptyInput < ::StandardError
17
+ class UserCancelled < DoingNoTraceError
18
+ def initialize(msg = 'Cancelled', topic = 'Exited:')
19
+ super(msg, level: :warn, topic: topic, exit_code: 1)
20
+ end
21
+ end
22
+
23
+ class EmptyInput < DoingNoTraceError
14
24
  def initialize(msg = 'No input', topic = 'Exited:')
15
- Doing.logger.output_results
16
- Doing.logger.log_now(:warn, topic, msg)
17
- Process.exit 1
25
+ super(msg, level: :warn, topic: topic, exit_code: 6)
18
26
  end
19
27
  end
20
28
 
@@ -22,44 +30,47 @@ module Doing
22
30
  def initialize(msg = '')
23
31
  Doing.logger.output_results
24
32
 
25
- super
33
+ super(msg)
26
34
  end
27
35
  end
28
36
 
29
- class WrongCommand < ::StandardError
37
+ class WrongCommand < DoingNoTraceError
30
38
  def initialize(msg = 'wrong command', topic: 'Error:')
31
- Doing.logger.warn(topic, msg)
32
-
33
- super(msg)
39
+ super(msg, level: :warn, topic: topic, exit_code: 2)
34
40
  end
35
41
  end
36
42
 
37
43
  class DoingRuntimeError < ::RuntimeError
38
- def initialize(msg = 'Runtime Error', topic: 'Error:')
44
+ def initialize(msg = 'Runtime Error', exit_code = nil, topic: 'Error:')
39
45
  Doing.logger.output_results
40
46
  Doing.logger.log_now(:error, topic, msg)
41
- Process.exit 1
47
+
48
+ Process.exit exit_code if exit_code
42
49
  end
43
50
  end
44
51
 
45
- class NoResults < ::StandardError
52
+ class NoResults < DoingNoTraceError
46
53
  def initialize(msg = 'No results', topic = 'Exited:')
47
- Doing.logger.output_results
48
- Doing.logger.log_now(:warn, topic, msg)
49
- Process.exit 0
54
+ super(msg, level: :warn, topic: topic, exit_code: 0)
50
55
 
51
56
  end
52
57
  end
53
58
 
54
- class DoingNoTraceError < ::StandardError
55
- def initialize(msg = nil, level = nil, topic = nil)
56
- level ||= :error
57
- Doing.logger.output_results
58
- if msg
59
- Doing.logger.log_now(level, topic, msg)
60
- end
59
+ class HistoryLimitError < DoingNoTraceError
60
+ def initialize(msg, exit_code = 24)
61
+ super(msg, level: :error, topic: 'History:', exit_code: exit_code)
62
+ end
63
+ end
61
64
 
62
- Process.exit 1
65
+ class MissingBackupFile < DoingNoTraceError
66
+ def initialize(msg, exit_code = 26)
67
+ super(msg, level: :error, topic: 'History:', exit_code: exit_code)
68
+ end
69
+ end
70
+
71
+ class InvalidPlugin < DoingRuntimeError
72
+ def initialize(kind = 'output', msg = nil)
73
+ super(%(Invalid #{kind} type (#{msg})), 128, topic: 'Invalid:')
63
74
  end
64
75
  end
65
76
 
@@ -75,6 +86,10 @@ module Doing
75
86
  'Import plugin'
76
87
  when /^e/
77
88
  'Export plugin'
89
+ when /^h/
90
+ 'Hook'
91
+ when /^u/
92
+ 'Unrecognized'
78
93
  else
79
94
  type.to_s
80
95
  end
@@ -82,7 +97,8 @@ module Doing
82
97
  msg = "(#{@type}: #{@plugin}) #{msg}"
83
98
 
84
99
  Doing.logger.log_now(:error, 'Plugin:', msg)
85
- Process.exit 1
100
+
101
+ super(msg)
86
102
  end
87
103
  end
88
104
 
@@ -90,17 +106,17 @@ module Doing
90
106
  InvalidPluginType = Class.new(PluginException)
91
107
  PluginUncallable = Class.new(PluginException)
92
108
 
93
- InvalidArgument = Class.new(DoingRuntimeError)
94
- MissingArgument = Class.new(DoingRuntimeError)
95
- MissingFile = Class.new(DoingRuntimeError)
96
- MissingEditor = Class.new(DoingRuntimeError)
109
+ InvalidArgument = Class.new(DoingNoTraceError)
110
+ MissingArgument = Class.new(DoingNoTraceError)
111
+ MissingFile = Class.new(DoingNoTraceError)
112
+ MissingEditor = Class.new(DoingNoTraceError)
97
113
  NonInteractive = Class.new(StandardError)
98
114
 
99
- NoEntryError = Class.new(DoingRuntimeError)
115
+ NoEntryError = Class.new(DoingNoTraceError)
100
116
 
101
117
  InvalidTimeExpression = Class.new(DoingRuntimeError)
102
- InvalidSection = Class.new(DoingRuntimeError)
103
- InvalidView = Class.new(DoingRuntimeError)
118
+ InvalidSection = Class.new(DoingNoTraceError)
119
+ InvalidView = Class.new(DoingNoTraceError)
104
120
 
105
121
  ItemNotFound = Class.new(DoingRuntimeError)
106
122
  # FatalException = Class.new(::RuntimeError)
data/lib/doing/hooks.rb CHANGED
@@ -15,7 +15,7 @@ module Doing
15
15
  post_entry_removed: [], # wwid, entry.dup
16
16
  pre_export: [], # wwid, format, entries
17
17
  pre_write: [], # wwid, file
18
- post_write: [] # wwid, file
18
+ post_write: [] # file
19
19
  }
20
20
 
21
21
  # map of all hooks and their priorities
@@ -40,10 +40,10 @@ module Doing
40
40
  # register a single hook to be called later, internal API
41
41
  def self.register_one(event, priority, &block)
42
42
  unless @registry[event]
43
- raise Doing::Errors::HookUnavailable, "Invalid hook. Doing only supports #{@registry.keys.inspect}"
43
+ raise Doing::Errors::HookUnavailable.new("Invalid hook. Doing only supports #{@registry.keys.inspect}", 'hook', event)
44
44
  end
45
45
 
46
- raise Doing::Errors::PluginUncallable, 'Hooks must respond to :call' unless block.respond_to? :call
46
+ raise Doing::Errors::PluginUncallable.new('Hooks must respond to :call', 'hook', event) unless block.respond_to? :call
47
47
 
48
48
  Doing.logger.debug('Hook Manager:', "Registered #{event} hook") if ENV['DOING_PLUGIN_DEBUG']
49
49
 
@@ -0,0 +1,112 @@
1
+ module Doing
2
+ class Item
3
+ # def date=(new_date)
4
+ # @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
5
+ # end
6
+
7
+ ## If the entry doesn't have a @done date, return the elapsed time
8
+ def duration
9
+ return nil unless should_time? && should_finish?
10
+
11
+ return nil if @title =~ /(?<=^| )@done\b/
12
+
13
+ return Time.now - @date
14
+ end
15
+
16
+ ##
17
+ ## Get the difference between the item's start date and
18
+ ## the value of its @done tag (if present)
19
+ ##
20
+ ## @return Interval in seconds
21
+ ##
22
+ def interval
23
+ @interval ||= calc_interval
24
+ end
25
+
26
+ ##
27
+ ## Get the value of the item's @done tag
28
+ ##
29
+ ## @return [Time] @done value
30
+ ##
31
+ def end_date
32
+ @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
33
+ end
34
+
35
+ def calculate_end_date(opt)
36
+ if opt[:took]
37
+ if @date + opt[:took] > Time.now
38
+ @date = Time.now - opt[:took]
39
+ Time.now
40
+ else
41
+ @date + opt[:took]
42
+ end
43
+ elsif opt[:back]
44
+ if opt[:back].is_a? Integer
45
+ @date + opt[:back]
46
+ else
47
+ @date + (opt[:back] - @date)
48
+ end
49
+ else
50
+ Time.now
51
+ end
52
+ end
53
+
54
+ ##
55
+ ## Test if two items occur at the same time (same start date and equal duration)
56
+ ##
57
+ ## @param item_b [Item] The item to compare
58
+ ##
59
+ ## @return [Boolean] is equal?
60
+ ##
61
+ def same_time?(item_b)
62
+ date == item_b.date ? interval == item_b.interval : false
63
+ end
64
+
65
+ ##
66
+ ## Test if the interval between start date and @done
67
+ ## value overlaps with another item's
68
+ ##
69
+ ## @param item_b [Item] The item to compare
70
+ ##
71
+ ## @return [Boolean] overlaps?
72
+ ##
73
+ def overlapping_time?(item_b)
74
+ return true if same_time?(item_b)
75
+
76
+ start_a = date
77
+ a_interval = interval
78
+ end_a = a_interval ? start_a + a_interval.to_i : start_a
79
+ start_b = item_b.date
80
+ b_interval = item_b.interval
81
+ end_b = b_interval ? start_b + b_interval.to_i : start_b
82
+ (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
83
+ end
84
+
85
+ ##
86
+ ## Updates the title of the Item by expanding natural
87
+ ## language dates within configured date tags (tags
88
+ ## whose value is expected to be a date)
89
+ ##
90
+ ## @param additional_tags An array of additional
91
+ ## tag names to consider
92
+ ## dates
93
+ ##
94
+ def expand_date_tags(additional_tags = nil)
95
+ @title.expand_date_tags(additional_tags)
96
+ end
97
+
98
+ private
99
+
100
+ def calc_interval
101
+ return nil unless should_time? && should_finish?
102
+
103
+ done = end_date
104
+ return nil if done.nil?
105
+
106
+ start = @date
107
+
108
+ t = (done - start).to_i
109
+ t.positive? ? t : nil
110
+ end
111
+ end
112
+ end