ruby-pomodoro 0.4.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 +7 -0
- data/.github/workflows/ruby.yml +24 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +78 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/ruby-pomodoro +5 -0
- data/lib/ruby/pomodoro/cmd/base.rb +40 -0
- data/lib/ruby/pomodoro/cmd/choose_task.rb +33 -0
- data/lib/ruby/pomodoro/cmd/edit_list.rb +15 -0
- data/lib/ruby/pomodoro/cmd/error_handler.rb +17 -0
- data/lib/ruby/pomodoro/cmd/main.rb +15 -0
- data/lib/ruby/pomodoro/cmd/pause.rb +27 -0
- data/lib/ruby/pomodoro/cmd/quit.rb +19 -0
- data/lib/ruby/pomodoro/cmd/stop.rb +12 -0
- data/lib/ruby/pomodoro/command_controller.rb +42 -0
- data/lib/ruby/pomodoro/notification.rb +40 -0
- data/lib/ruby/pomodoro/notification_observer.rb +32 -0
- data/lib/ruby/pomodoro/printer.rb +56 -0
- data/lib/ruby/pomodoro/progressbar.rb +32 -0
- data/lib/ruby/pomodoro/task.rb +25 -0
- data/lib/ruby/pomodoro/tasks/editor.rb +65 -0
- data/lib/ruby/pomodoro/tasks/entity.rb +27 -0
- data/lib/ruby/pomodoro/tasks/resource.rb +42 -0
- data/lib/ruby/pomodoro/terminal_notifier_channel.rb +13 -0
- data/lib/ruby/pomodoro/time_converter.rb +27 -0
- data/lib/ruby/pomodoro/time_helpers.rb +11 -0
- data/lib/ruby/pomodoro/version.rb +5 -0
- data/lib/ruby/pomodoro/worker.rb +78 -0
- data/lib/ruby/pomodoro.rb +86 -0
- data/lib/view/main.txt.erb +13 -0
- data/ruby-pomodoro.gemspec +31 -0
- metadata +156 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
class NotificationObserver
|
4
|
+
# @param stop [Ruby::Pomodoro::Notification]
|
5
|
+
# @param pause [Ruby::Pomodoro::Notification]
|
6
|
+
# @param time [Numeric] repeat notifications interval in seconds
|
7
|
+
def initialize(stop:, pause:, time:, printer: Printer.new)
|
8
|
+
@stop_notification = stop
|
9
|
+
@pause_notification = pause
|
10
|
+
@time = time
|
11
|
+
@printer = printer
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Symbol] event
|
15
|
+
def update(event)
|
16
|
+
case event
|
17
|
+
when :finish
|
18
|
+
@stop_notification.notify(@time)
|
19
|
+
@printer.print_line "_Task #{Worker.instance.current_task.name} was stopped, type [\u21e7 + R] for resume\r",
|
20
|
+
color: :red
|
21
|
+
when :pause
|
22
|
+
@pause_notification.notify(@time, skip_now: true)
|
23
|
+
when :stop, :start
|
24
|
+
@pause_notification.stop
|
25
|
+
@stop_notification.stop
|
26
|
+
else
|
27
|
+
@pause_notification.stop
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
class Printer
|
4
|
+
def initialize(stream: $stdout, cursor: TTY::Cursor, palette: Pastel.new)
|
5
|
+
@stream = stream
|
6
|
+
@cursor = cursor
|
7
|
+
@palette = palette
|
8
|
+
end
|
9
|
+
|
10
|
+
def print_template(template_name, cmd_binding)
|
11
|
+
@__cmd_binding = cmd_binding
|
12
|
+
stream.print(render(template_name))
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear_terminal
|
16
|
+
stream.print(cursor.up(100))
|
17
|
+
stream.print(cursor.clear_screen_down)
|
18
|
+
end
|
19
|
+
|
20
|
+
def print(text, color: nil)
|
21
|
+
if color
|
22
|
+
stream.print(palette.send(color, text))
|
23
|
+
else
|
24
|
+
stream.print(text)
|
25
|
+
end
|
26
|
+
stream.flush
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_line(text, color: nil)
|
30
|
+
cursor.clear_line
|
31
|
+
print(text, color: color)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def render(file_name)
|
36
|
+
ERB.new(read_file(file_name)).result(@__cmd_binding)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :stream, :cursor, :palette
|
42
|
+
|
43
|
+
|
44
|
+
def read_file(file_name_or_path)
|
45
|
+
if file_name_or_path.kind_of?(Symbol)
|
46
|
+
path = File.expand_path(
|
47
|
+
"../../../#{File.join("view", "#{file_name_or_path}.txt.erb")}", __FILE__
|
48
|
+
)
|
49
|
+
File.read(path)
|
50
|
+
else
|
51
|
+
File.read(file_name_or_path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
class Progressbar
|
4
|
+
def initialize(seconds:, printer: Ruby::Pomodoro::Printer.new)
|
5
|
+
@all_seconds = seconds
|
6
|
+
@spent_seconds = 0
|
7
|
+
@printer = printer
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(text)
|
11
|
+
@text = text
|
12
|
+
end
|
13
|
+
|
14
|
+
def increment
|
15
|
+
@spent_seconds += 1
|
16
|
+
print
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def print
|
22
|
+
@printer.print " In progress: #{@text} [#{strftime}]\r", color: :green
|
23
|
+
end
|
24
|
+
|
25
|
+
def strftime
|
26
|
+
seconds = @all_seconds - @spent_seconds
|
27
|
+
minutes = seconds / 60
|
28
|
+
"#{minutes} m #{seconds - minutes * 60} s"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
# Task for [Ruby::Pomodoro::Worker]
|
4
|
+
class Task
|
5
|
+
attr_reader :name, :pomodors, :spent_time
|
6
|
+
|
7
|
+
def initialize(name, spent_time: 0)
|
8
|
+
@name = name
|
9
|
+
@spent_time = spent_time
|
10
|
+
@pomodors = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Integer]
|
14
|
+
def add_pomodoro
|
15
|
+
@pomodors += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [Integer] seconds
|
19
|
+
# @return [Integer]
|
20
|
+
def track(seconds)
|
21
|
+
@spent_time += seconds
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module Tasks
|
4
|
+
class Editor
|
5
|
+
attr_reader :tasks_repo, :editor, :file_path
|
6
|
+
|
7
|
+
# @param [Array<Ruby::Pomodoro::Task>] tasks_repo
|
8
|
+
# @param [String] file_path
|
9
|
+
# @param editor [Class, Object] Editor with method open, for create and open file
|
10
|
+
def initialize(tasks_repo: Resource, file_path: Ruby::Pomodoro.tasks_file_path, editor: TTY::Editor)
|
11
|
+
@tasks_repo = tasks_repo
|
12
|
+
@file_path = file_path
|
13
|
+
@editor = editor
|
14
|
+
end
|
15
|
+
|
16
|
+
# Open editor and save tasks from tmp file to task_repo
|
17
|
+
# @return [TrueClass]
|
18
|
+
def edit
|
19
|
+
save
|
20
|
+
editor.open(file_path)
|
21
|
+
create_tasks
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
# save list to disc
|
26
|
+
# @return [TrueClass]
|
27
|
+
def save
|
28
|
+
File.open(file_path, "w") do |f|
|
29
|
+
if @tasks_repo.size > 0
|
30
|
+
f.puts(@tasks_repo.all.map {|task| print_task(task) }.join("\n"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Load tasks form file
|
37
|
+
# @return [TrueClass]
|
38
|
+
def load
|
39
|
+
create_tasks
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def read_tmp_file
|
46
|
+
File.readlines(file_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_tasks
|
50
|
+
tasks_repo.delete_all
|
51
|
+
read_tmp_file.each do |line|
|
52
|
+
next unless line
|
53
|
+
|
54
|
+
name, time = line.split("|").compact.map {|l| l.chomp.strip }
|
55
|
+
tasks_repo.create(name: name, spent_time: TimeConverter.to_seconds(time))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def print_task(task)
|
60
|
+
"#{task.name} | #{TimeConverter.to_format_string(task.spent_time)}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module Tasks
|
4
|
+
class Entity
|
5
|
+
attr_reader :name, :pomodors, :spent_time, :id
|
6
|
+
|
7
|
+
def initialize(name:, spent_time: 0, id: nil)
|
8
|
+
@name = name
|
9
|
+
@spent_time = spent_time
|
10
|
+
@pomodors = 0
|
11
|
+
@id = id
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Integer]
|
15
|
+
def add_pomodoro
|
16
|
+
@pomodors += 1
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Integer] seconds
|
20
|
+
# @return [Integer]
|
21
|
+
def track(seconds)
|
22
|
+
@spent_time += seconds
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module Tasks
|
4
|
+
class Resource
|
5
|
+
class << self
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
delegate size: :store
|
9
|
+
|
10
|
+
def create(attributes)
|
11
|
+
id = store.size.next
|
12
|
+
Entity.new(id: id, **attributes.slice(:name, :spent_time)).tap do |task|
|
13
|
+
store[id] = task
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(id)
|
18
|
+
store[id]
|
19
|
+
end
|
20
|
+
|
21
|
+
def all
|
22
|
+
store.values
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete_all
|
26
|
+
store.clear
|
27
|
+
end
|
28
|
+
|
29
|
+
def sum(attribute)
|
30
|
+
all.inject(0) { |sum, task| sum + task.public_send(attribute).to_i }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def store
|
36
|
+
@store ||= {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module TimeConverter
|
4
|
+
class << self
|
5
|
+
def to_format_string(seconds)
|
6
|
+
days = seconds / 60 / 60 / 24
|
7
|
+
hours = (seconds - days * 24 * 60 * 60) / 60 / 60
|
8
|
+
minutes = (seconds - (hours * 60 * 60) - (days * 24 * 60 * 60)) / 60
|
9
|
+
{d: days, h: hours, m: minutes}.select { |_k, v| v.positive? }.each.with_object(String.new) do |item, acc|
|
10
|
+
acc << item.reverse.join(":") + " "
|
11
|
+
end.strip
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_seconds(format_string)
|
15
|
+
return 0 if format_string.nil?
|
16
|
+
|
17
|
+
match = format_string.split(" ").compact.map do |item|
|
18
|
+
if item
|
19
|
+
item.split(":").reverse.then { |res| res.size == 2 ? res : nil }
|
20
|
+
end
|
21
|
+
end.compact.to_h.select { |k| %w[m h d].include?(k) }
|
22
|
+
(match["d"].to_i * 24 * 60 * 60) + (match["h"].to_i * 60 * 60) + (match["m"].to_i * 60)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
# Singleton for work with tasks in application
|
4
|
+
class Worker
|
5
|
+
include Observable
|
6
|
+
include AASM
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
attr_reader :current_task
|
10
|
+
attr_accessor :time_interval, :pomodoro_size, :progressbar
|
11
|
+
|
12
|
+
aasm do
|
13
|
+
state :sleeping, initial: true
|
14
|
+
state :working, :paused
|
15
|
+
|
16
|
+
after_all_transitions :notify
|
17
|
+
|
18
|
+
event :start, before: :changed do
|
19
|
+
transitions from: :sleeping, to: :working, after: ->(task) { run_task(task) }
|
20
|
+
end
|
21
|
+
|
22
|
+
event :resume, before: :changed do
|
23
|
+
transitions from: :paused, to: :working
|
24
|
+
end
|
25
|
+
|
26
|
+
event :pause, before: :changed do
|
27
|
+
transitions from: :working, to: :paused
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
event :finish, before: :changed do
|
32
|
+
after { @do&.kill }
|
33
|
+
|
34
|
+
transitions from: :working, to: :sleeping
|
35
|
+
end
|
36
|
+
|
37
|
+
event :stop, before: :changed do
|
38
|
+
after do
|
39
|
+
@do&.kill
|
40
|
+
@current_task = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
transitions to: :sleeping
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def notify
|
50
|
+
notify_observers(aasm.current_event)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Ruby::Pomodoro::Task] task
|
54
|
+
# @return [TrueClass]
|
55
|
+
def run_task(task)
|
56
|
+
raise Error, "Setup pomodoro_size" if pomodoro_size.nil?
|
57
|
+
|
58
|
+
@current_task = task
|
59
|
+
@do = Thread.new do
|
60
|
+
progress = progressbar || Ruby::Pomodoro::Progressbar.new(seconds: pomodoro_size)
|
61
|
+
progress.start(task.name)
|
62
|
+
count = 0
|
63
|
+
loop do
|
64
|
+
seconds = time_interval || 1
|
65
|
+
sleep seconds
|
66
|
+
unless paused?
|
67
|
+
task.track(seconds)
|
68
|
+
count += seconds
|
69
|
+
progress.increment
|
70
|
+
finish if count >= pomodoro_size.to_i && working?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'terminal-notifier'
|
2
|
+
require 'observer'
|
3
|
+
require 'tty-cursor'
|
4
|
+
require 'tty-editor'
|
5
|
+
require 'tty-reader'
|
6
|
+
require 'logger'
|
7
|
+
require 'aasm'
|
8
|
+
require "erb"
|
9
|
+
require "ruby/pomodoro/command_controller"
|
10
|
+
require "ruby/pomodoro/time_helpers"
|
11
|
+
require "ruby/pomodoro/notification_observer"
|
12
|
+
require "ruby/pomodoro/printer"
|
13
|
+
require "ruby/pomodoro/cmd/base"
|
14
|
+
require "ruby/pomodoro/cmd/main"
|
15
|
+
require "ruby/pomodoro/cmd/quit"
|
16
|
+
require "ruby/pomodoro/cmd/choose_task"
|
17
|
+
require "ruby/pomodoro/cmd/edit_list"
|
18
|
+
require "ruby/pomodoro/cmd/pause"
|
19
|
+
require "ruby/pomodoro/cmd/stop"
|
20
|
+
require "ruby/pomodoro/cmd/error_handler"
|
21
|
+
require "ruby/pomodoro/time_converter"
|
22
|
+
require "ruby/pomodoro/version"
|
23
|
+
require "ruby/pomodoro/tasks/resource"
|
24
|
+
require "ruby/pomodoro/tasks/editor"
|
25
|
+
require "ruby/pomodoro/task"
|
26
|
+
require "ruby/pomodoro/tasks/entity"
|
27
|
+
require "ruby/pomodoro/worker"
|
28
|
+
require "ruby/pomodoro/progressbar"
|
29
|
+
require "ruby/pomodoro/notification"
|
30
|
+
require "ruby/pomodoro/terminal_notifier_channel"
|
31
|
+
|
32
|
+
module Ruby
|
33
|
+
module Pomodoro
|
34
|
+
class Error < StandardError; end
|
35
|
+
|
36
|
+
REPEAT_ALERT_TIME = 60 * 5
|
37
|
+
POMODORO_SIZE = 60 * 30
|
38
|
+
|
39
|
+
class << self
|
40
|
+
attr_reader :editor, :tasks_file_path
|
41
|
+
|
42
|
+
def start
|
43
|
+
Worker.instance.then do |worker|
|
44
|
+
add_observer(worker)
|
45
|
+
worker.pomodoro_size = POMODORO_SIZE
|
46
|
+
end
|
47
|
+
|
48
|
+
@tasks_file_path = File.join(init_app_folder, "tasks")
|
49
|
+
|
50
|
+
@editor = Tasks::Editor.new(file_path: tasks_file_path)
|
51
|
+
editor.load
|
52
|
+
Cmd::Main.new.call
|
53
|
+
reader = TTY::Reader.new
|
54
|
+
reader.subscribe(CommandController)
|
55
|
+
loop do
|
56
|
+
reader.read_char
|
57
|
+
rescue => e
|
58
|
+
Cmd::ErrorHandler.new.call(e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def add_observer(worker)
|
65
|
+
notify_ch = TerminalNotifierChannel
|
66
|
+
pause_notification = Notification.new("Task is paused, resume?", notify_ch)
|
67
|
+
stop_notification =
|
68
|
+
Notification.new("Work is stopped, choose task for resume", notify_ch)
|
69
|
+
|
70
|
+
worker.add_observer(
|
71
|
+
NotificationObserver.new(
|
72
|
+
stop: stop_notification, pause: pause_notification, time: REPEAT_ALERT_TIME
|
73
|
+
)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def init_app_folder
|
78
|
+
path = File.join(Dir.home, ".ruby-pomodoro")
|
79
|
+
unless Dir.exists?(path)
|
80
|
+
Dir.mkdir(path)
|
81
|
+
end
|
82
|
+
path
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Total time: <%= to_format_string(Ruby::Pomodoro::Tasks::Resource.sum(:spent_time)) %>
|
2
|
+
List of tasks:
|
3
|
+
<% if @tasks %>
|
4
|
+
<% @tasks.each do |task| %>
|
5
|
+
<%= task.id %>. <%= task.name %> <%= task.spent_time.zero? ? nil : to_format_string(task.spent_time) %>
|
6
|
+
<% end %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
[c] - Choose task to work
|
10
|
+
[e] - Edit list of tasks
|
11
|
+
[p] - Pause work
|
12
|
+
[s] - Stop work
|
13
|
+
[q] - Quit
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'lib/ruby/pomodoro/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "ruby-pomodoro"
|
5
|
+
spec.version = Ruby::Pomodoro::VERSION
|
6
|
+
spec.authors = ["Alexander Shvaykin"]
|
7
|
+
spec.email = ["skiline.alex@gmail.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{CLI pomodoro-tracker}
|
10
|
+
spec.homepage = "https://github.com/AlexanderShvaykin"
|
11
|
+
spec.license = "MIT"
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
13
|
+
|
14
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
15
|
+
spec.metadata["source_code_uri"] = "https://github.com/AlexanderShvaykin"
|
16
|
+
spec.metadata["changelog_uri"] = "https://github.com/AlexanderShvaykin"
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
spec.add_runtime_dependency 'terminal-notifier'
|
27
|
+
spec.add_runtime_dependency 'tty-cursor', '~> 0.6'
|
28
|
+
spec.add_runtime_dependency 'tty-editor', '~> 0.7'
|
29
|
+
spec.add_runtime_dependency 'pastel', '~> 0.8'
|
30
|
+
spec.add_runtime_dependency 'aasm', '~> 5'
|
31
|
+
end
|