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 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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/timert"
4
+
5
+ DATABASE_PATH = ENV["HOME"] + "/.timert"
6
+
7
+ app = Timert::Application.new(ARGV, DATABASE_PATH)
8
+ puts app.result["message"]
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
@@ -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