timert 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 +15 -0
- data/bin/timert +8 -0
- data/lib/timert.rb +10 -0
- data/lib/timert/application.rb +73 -0
- data/lib/timert/argument_parser.rb +53 -0
- data/lib/timert/database.rb +62 -0
- data/lib/timert/database_file.rb +20 -0
- data/lib/timert/date_util.rb +22 -0
- data/lib/timert/day.rb +109 -0
- data/lib/timert/duration.rb +24 -0
- data/lib/timert/help.rb +21 -0
- data/lib/timert/report.rb +104 -0
- data/lib/timert/timer.rb +38 -0
- data/spec/application_spec.rb +148 -0
- data/spec/argument_parser_spec.rb +47 -0
- data/spec/database_file_spec.rb +21 -0
- data/spec/database_spec.rb +58 -0
- data/spec/date_util_spec.rb +19 -0
- data/spec/day_spec.rb +220 -0
- data/spec/duration_spec.rb +40 -0
- data/spec/report_spec.rb +83 -0
- data/spec/timer_spec.rb +63 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NjVmYmVjODViODExZGJkNmEyOGE5YWY5NjE3Nzg4NDcwODE3NjYwYw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ODljMWE2ZTk3NzY4ZjVkOGNkOThlNjRjNzhiZjY3ZDhkN2Q1N2Q3OA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
OTMyMWM4OTJlZjU3OTVjZjM5OThjMjg4YzUwZmJlZDUxZWM5YjYxYjJiYmQx
|
10
|
+
M2E2M2RjZTk3Y2U4YjdjNTA1ZWIwNDkxMGZkN2NiOTlkYjYzNjE4ODE0OTI4
|
11
|
+
ZWI5YzJkODVlMTk5MjZlNTQ4OTg1NmU4Njc1N2RiNTA0MmVlMTU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTIwOTk3NGYwY2ZmMmZjODg2MDE3YmIzOGEyYTg0ZTUwMmQ3NjAzZmNkZTZm
|
14
|
+
ZTI1ZTJkMDYwNzEwZjMzOTcwNWFlZWMxNzhjZmExN2I1MDhjODA5ZWI4Mzk2
|
15
|
+
MWQxYzZlZTQ3OGNjNjQ1MDJiY2EyNmUxNzhmZjNjMGQyOGI4Nzc=
|
data/bin/timert
ADDED
data/lib/timert.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative 'timert/application'
|
2
|
+
require_relative 'timert/argument_parser'
|
3
|
+
require_relative 'timert/database'
|
4
|
+
require_relative 'timert/database_file'
|
5
|
+
require_relative 'timert/date_util'
|
6
|
+
require_relative 'timert/day'
|
7
|
+
require_relative 'timert/duration'
|
8
|
+
require_relative 'timert/help'
|
9
|
+
require_relative 'timert/report'
|
10
|
+
require_relative 'timert/timer'
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require_relative 'argument_parser'
|
2
|
+
require_relative 'timer'
|
3
|
+
require_relative 'date_util'
|
4
|
+
require_relative 'database'
|
5
|
+
require_relative 'database_file'
|
6
|
+
require_relative 'report'
|
7
|
+
require_relative 'help'
|
8
|
+
|
9
|
+
module Timert
|
10
|
+
class Application
|
11
|
+
attr_reader :result
|
12
|
+
|
13
|
+
def initialize(argv, db_path)
|
14
|
+
@database = Database.new(DatabaseFile.new(db_path))
|
15
|
+
@timer = Timer.new(@database.today)
|
16
|
+
@result = {}
|
17
|
+
|
18
|
+
parser = ArgumentParser.new(argv)
|
19
|
+
send(parser.action, *[parser.argument].compact)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def start(time = nil)
|
24
|
+
begin
|
25
|
+
timer_result = @timer.start(time)
|
26
|
+
if timer_result[:started]
|
27
|
+
add_message("start timer at #{format_hour(timer_result[:time])}")
|
28
|
+
@database.save(@timer.today)
|
29
|
+
else
|
30
|
+
add_message("timer already started at #{format_hour(timer_result[:time])}")
|
31
|
+
end
|
32
|
+
rescue ArgumentError => e
|
33
|
+
add_message(e.message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop(time = nil)
|
38
|
+
begin
|
39
|
+
timer_result = @timer.stop(time)
|
40
|
+
if timer_result[:stopped]
|
41
|
+
add_message("stop timer at #{format_hour(timer_result[:time])}")
|
42
|
+
@database.save(@timer.today)
|
43
|
+
else
|
44
|
+
add_message("timer isn't started yet")
|
45
|
+
end
|
46
|
+
rescue ArgumentError => e
|
47
|
+
add_message(e.message)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def report(time_expression)
|
52
|
+
add_message(Report.generate(@database, time_expression))
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_task(task)
|
56
|
+
add_message("added task: #{task}")
|
57
|
+
@timer.add_task(task)
|
58
|
+
@database.save(@timer.today)
|
59
|
+
end
|
60
|
+
|
61
|
+
def help
|
62
|
+
add_message(Help.generate)
|
63
|
+
end
|
64
|
+
|
65
|
+
def format_hour(timestamp)
|
66
|
+
DateUtil.format_hour(timestamp)
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_message(msg)
|
70
|
+
@result["message"] = msg
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative "date_util"
|
2
|
+
|
3
|
+
module Timert
|
4
|
+
class ArgumentParser
|
5
|
+
attr_reader :action, :argument
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
if args.empty?
|
9
|
+
@action = "help"
|
10
|
+
@argument = nil
|
11
|
+
elsif is_api?(args[0])
|
12
|
+
@action = args[0]
|
13
|
+
@argument = api_argument(@action, args[1])
|
14
|
+
else
|
15
|
+
@action = 'add_task'
|
16
|
+
@argument = args.join(" ")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def is_api?(method_name)
|
22
|
+
["start", "stop", "report"].include?(method_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def api_argument(action, arg)
|
26
|
+
if is_time_method?(action)
|
27
|
+
parse_time(arg)
|
28
|
+
elsif is_report?(action)
|
29
|
+
parse_report_arg(arg)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def is_time_method?(method_name)
|
34
|
+
["start", "stop"].include?(method_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_report?(method_name)
|
38
|
+
method_name == "report"
|
39
|
+
end
|
40
|
+
|
41
|
+
def is_month_or_week?(arg)
|
42
|
+
arg == "month" || arg == "week"
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_report_arg(arg)
|
46
|
+
is_month_or_week?(arg) ? arg : arg.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_time(arg)
|
50
|
+
DateUtil.parse_time(arg)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'day'
|
3
|
+
|
4
|
+
module Timert
|
5
|
+
class Database
|
6
|
+
|
7
|
+
def initialize(file)
|
8
|
+
@file = file
|
9
|
+
end
|
10
|
+
|
11
|
+
def today
|
12
|
+
day
|
13
|
+
end
|
14
|
+
|
15
|
+
def day(date = Date.today)
|
16
|
+
hash_to_day(load_data[key(date)], date)
|
17
|
+
end
|
18
|
+
|
19
|
+
def days(range)
|
20
|
+
entries = load_data
|
21
|
+
result = []
|
22
|
+
entries.each_pair do |date, day_hash|
|
23
|
+
day = hash_to_day(day_hash, Time.at(date.to_i).to_date)
|
24
|
+
if range.include?(day.date)
|
25
|
+
result << day
|
26
|
+
end
|
27
|
+
end
|
28
|
+
result
|
29
|
+
end
|
30
|
+
|
31
|
+
def save(day)
|
32
|
+
current_data = load_data
|
33
|
+
current_data[key(day.date)] = day.to_hash
|
34
|
+
save_data(current_data)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def load_data
|
40
|
+
@file.load
|
41
|
+
end
|
42
|
+
|
43
|
+
def save_data(hash)
|
44
|
+
@file.save(hash)
|
45
|
+
end
|
46
|
+
|
47
|
+
def key(date = nil)
|
48
|
+
date ? date.to_time.to_i.to_s : key(Date.today)
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash_to_day(day_hash, date)
|
52
|
+
if day_hash
|
53
|
+
Day.new(
|
54
|
+
intervals: day_hash["intervals"],
|
55
|
+
tasks: day_hash["tasks"],
|
56
|
+
date: date)
|
57
|
+
else
|
58
|
+
Day.new
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Timert
|
4
|
+
class DatabaseFile
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def load
|
11
|
+
File.open(@path, 'a+') do |file|
|
12
|
+
file.size > 0 ? JSON.load(file) : {}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def save(hash)
|
17
|
+
File.open(@path, 'w+') { |f| f.write(hash.to_json) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Timert
|
4
|
+
class DateUtil
|
5
|
+
|
6
|
+
def self.format_hour(timestamp)
|
7
|
+
timestamp ? Time.at(timestamp).strftime("%H:%M:%S") : ""
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.format_date(date)
|
11
|
+
date ? date.strftime("%Y-%m-%d") : ""
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.parse_time(arg)
|
15
|
+
if arg
|
16
|
+
hours, minutes = arg.split(":")
|
17
|
+
now = Time.now
|
18
|
+
Time.new(now.year, now.month, now.day, hours, minutes).to_i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/timert/day.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module Timert
|
2
|
+
class Day
|
3
|
+
|
4
|
+
attr_reader :intervals, :tasks, :date
|
5
|
+
attr_accessor :on
|
6
|
+
|
7
|
+
def initialize(args = {})
|
8
|
+
@intervals = args[:intervals] || []
|
9
|
+
@tasks = args[:tasks] || []
|
10
|
+
@date = args[:date]
|
11
|
+
|
12
|
+
raise ArgumentError.new("intervals should be an array") if !@intervals.is_a?(Array)
|
13
|
+
raise ArgumentError.new("tasks should be an array") if !@tasks.is_a?(Array)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_start(time)
|
17
|
+
if !is_interval_started?
|
18
|
+
if time <= last_start
|
19
|
+
raise ArgumentError.new("Invalid start time")
|
20
|
+
elsif time < last_stop
|
21
|
+
raise ArgumentError.new("Invalid start time. It's before the last stop time.")
|
22
|
+
elsif !is_date_correct?(time)
|
23
|
+
raise ArgumentError.new("Invalid date")
|
24
|
+
end
|
25
|
+
@intervals.push({"start" => time})
|
26
|
+
time
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_stop(time)
|
31
|
+
if is_interval_started?
|
32
|
+
if time < last_start
|
33
|
+
raise ArgumentError.new("Invalid stop time")
|
34
|
+
elsif !is_date_correct?(time)
|
35
|
+
raise ArgumentError.new("Invalid date")
|
36
|
+
end
|
37
|
+
@intervals.last["stop"] = time
|
38
|
+
time
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def total_elapsed_time
|
43
|
+
total = 0
|
44
|
+
@intervals.each { |i| total += interval_duration(i) }
|
45
|
+
total
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_task(task)
|
49
|
+
@tasks.push(task)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_hash
|
53
|
+
{
|
54
|
+
"tasks" => @tasks.uniq,
|
55
|
+
"intervals" => @intervals
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def is_interval_started?
|
60
|
+
@intervals.length > 0 &&
|
61
|
+
@intervals.last["start"] &&
|
62
|
+
!@intervals.last["stop"]
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(other)
|
66
|
+
other.to_hash == to_hash
|
67
|
+
end
|
68
|
+
|
69
|
+
def last_start
|
70
|
+
last_interval['start'].to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
def last_stop
|
74
|
+
last_interval['stop'].to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def interval_duration(interval)
|
79
|
+
start, stop = interval["start"], interval["stop"]
|
80
|
+
if start
|
81
|
+
stop ||= interval_end_when_start(start)
|
82
|
+
stop.to_i - start.to_i
|
83
|
+
else
|
84
|
+
0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def last_interval
|
89
|
+
@intervals.length > 0 ? @intervals.last : {}
|
90
|
+
end
|
91
|
+
|
92
|
+
def is_date_correct?(timestamp)
|
93
|
+
!@date || Time.at(timestamp).to_date == @date
|
94
|
+
end
|
95
|
+
|
96
|
+
def interval_end_when_start(timestamp)
|
97
|
+
day_is_today?(timestamp) ? Time.now.to_i : last_second_of_day(timestamp)
|
98
|
+
end
|
99
|
+
|
100
|
+
def day_is_today?(timestamp)
|
101
|
+
Time.now.to_date == Time.at(timestamp).to_date
|
102
|
+
end
|
103
|
+
|
104
|
+
def last_second_of_day(timestamp)
|
105
|
+
time = Time.at(timestamp)
|
106
|
+
Time.new(time.year, time.month, time.day + 1, 0)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Timert
|
2
|
+
class Duration
|
3
|
+
attr_reader :hours, :minutes, :seconds, :value
|
4
|
+
|
5
|
+
def initialize(duration)
|
6
|
+
@hours = duration / 3600
|
7
|
+
@minutes = (duration % 3600) / 60
|
8
|
+
@seconds = duration % 60
|
9
|
+
@value = duration
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from(hours, minutes, seconds)
|
13
|
+
Duration.new(hours * 3600 + minutes * 60 + seconds)
|
14
|
+
end
|
15
|
+
|
16
|
+
def round
|
17
|
+
((value / 1800.0).round / 2.0).to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"#{hours}h #{minutes}min #{seconds}sec"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/timert/help.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Timert
|
2
|
+
class Help
|
3
|
+
def self.generate
|
4
|
+
"usage: timert <command> [ARG]\n\n"\
|
5
|
+
"List of commands:\n"\
|
6
|
+
" start [ARG] Starts the timer. [ARG]: time.\n"\
|
7
|
+
" stop [ARG] Stops the timer. [ARG]: time.\n"\
|
8
|
+
" report [ARG] Displays a summary report. [ARG]: number, 'week' or 'month'.\n"\
|
9
|
+
" <anything else> Adds a task.\n\n"\
|
10
|
+
"Usage examples: \n"\
|
11
|
+
" timert start Starts the timer at the current time.\n"\
|
12
|
+
" timert start 12:20 Starts the timer at the given time.\n"\
|
13
|
+
" timert stop 14 Stops the timer at the given time.\n"\
|
14
|
+
" timert report Displays a summary for today.\n"\
|
15
|
+
" timert report -1 Displays a summary for yesterday.\n"\
|
16
|
+
" timert report week Displays a summary for this week.\n"\
|
17
|
+
" timert report month Displays a summary report for this month.\n"\
|
18
|
+
" timert writing emails Adds a task: 'writing emails'."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
require 'date'
|
3
|
+
require_relative 'date_util'
|
4
|
+
require_relative 'duration'
|
5
|
+
|
6
|
+
module Timert
|
7
|
+
class Report
|
8
|
+
|
9
|
+
def self.generate(database, time_expression = "")
|
10
|
+
if time_expression == "month"
|
11
|
+
report_for_month(database)
|
12
|
+
elsif time_expression == "week"
|
13
|
+
report_for_week(database)
|
14
|
+
else
|
15
|
+
report_for_day(database, time_expression.to_i)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def self.report_for_week(database)
|
21
|
+
today = Date.today
|
22
|
+
first = today - today.cwday
|
23
|
+
last = first + 6
|
24
|
+
"REPORT FOR THIS WEEK\n".blue +
|
25
|
+
report_for_range(Range.new(first, last), database)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.report_for_month(database)
|
29
|
+
today = Date.today
|
30
|
+
first = Date.new(today.year, today.month, 1)
|
31
|
+
last = Date.new(today.year, today.month, -1)
|
32
|
+
"REPORT FOR THIS MONTH\n".blue +
|
33
|
+
report_for_range(Range.new(first, last), database)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.report_for_range(range, database)
|
37
|
+
days = database.days(range)
|
38
|
+
|
39
|
+
s = "\nDay/time elapsed\n".green
|
40
|
+
total_time, total_rounded_duration = 0, 0
|
41
|
+
|
42
|
+
days.each do |day|
|
43
|
+
duration = duration(day.total_elapsed_time)
|
44
|
+
s += "#{format_date(day.date)}: ".yellow +
|
45
|
+
"#{duration.to_s} / #{duration.round} " +
|
46
|
+
"(#{format_tasks(day)})\n"
|
47
|
+
total_time += duration.value
|
48
|
+
total_rounded_duration += duration.round.to_f
|
49
|
+
end
|
50
|
+
|
51
|
+
s += "\nTotal:\n".green
|
52
|
+
s += "#{parse_duration(total_time)} / #{total_rounded_duration}"
|
53
|
+
s
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.report_for_day(database, day_counter = 0)
|
57
|
+
date = Date.today + day_counter
|
58
|
+
day = database.day(date)
|
59
|
+
if day
|
60
|
+
"REPORT FOR #{format_date(date)}\n".blue +
|
61
|
+
"\nTasks:\n".green +
|
62
|
+
"#{format_tasks(day)}\n" +
|
63
|
+
"\nWork time:\n".green +
|
64
|
+
"#{format_intervals(day)}" +
|
65
|
+
"\nTotal elapsed time:\n".green +
|
66
|
+
"#{parse_duration(day.total_elapsed_time)}\n" +
|
67
|
+
"\nSummary:\n".red +
|
68
|
+
"#{round_duration(day.total_elapsed_time)} #{format_tasks(day)}"
|
69
|
+
else
|
70
|
+
"No data"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.format_tasks(day)
|
75
|
+
day.tasks.length > 0 ? day.tasks.join(", ") : "-"
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.format_intervals(day)
|
79
|
+
s = ""
|
80
|
+
day.intervals.each do |i|
|
81
|
+
start = DateUtil.format_hour(i["start"])
|
82
|
+
stop = DateUtil.format_hour(i["stop"])
|
83
|
+
s += "#{start} - #{stop}\n"
|
84
|
+
end
|
85
|
+
s
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.format_date(date)
|
89
|
+
DateUtil.format_date(date)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.parse_duration(duration)
|
93
|
+
duration(duration).to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.duration(duration)
|
97
|
+
Duration.new(duration)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.round_duration(duration)
|
101
|
+
duration(duration).round
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|