timert 1.0.0

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