timert 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|