class PunchCard
Constants
- HOURLY_RATE_PATTERN
- META_KEY_PATTERN
- SETTINGS_DIR
- TIME_POINT_PATTERN
- VERSION
Attributes
title[RW]
Public Class Methods
decimal_digits(digit)
click to toggle source
# File lib/punchcard.rb, line 214 def self.decimal_digits(digit) if digit.to_i < 10 "0#{digit}" else digit.to_s end end
format_time(datetime)
click to toggle source
# File lib/punchcard.rb, line 201 def self.format_time(datetime) datetime.strftime('%F %T') end
humanize_duration(duration)
click to toggle source
# File lib/punchcard.rb, line 205 def self.humanize_duration(duration) return nil unless duration hours = duration / (60 * 60) minutes = (duration / 60) % 60 seconds = duration % 60 "#{decimal_digits(hours)}:#{decimal_digits(minutes)}:#{decimal_digits(seconds)}" end
new(project_name)
click to toggle source
# File lib/punchcard.rb, line 20 def initialize(project_name) @wilcard_for_filename = '' @meta_data = {} find_or_make_settings_dir return unless project_name self.project = project_name find_or_make_file read_project_data end
Public Instance Methods
csv(start_at: nil, end_at: nil)
click to toggle source
# File lib/punchcard.rb, line 106 def csv(start_at: nil, end_at: nil) project_exists_or_stop! find_or_make_file durations = [] last_activity = nil project_data.map do |line| points = line_to_time_points(line) next unless points start_time = points[0] end_time = points[1] || timestamp next if time_range_is_excluded_by_filter?( start_at: start_at, end_at: end_at, start_time: start_time, end_time: end_time ) last_activity = points[1] || points[0] durations.push end_time - start_time end total_duration = self.class.humanize_duration( durations.reduce(&:+) || 0 ) '"' + [ title_and_project, running_status, last_activity ? self.class.format_time(Time.at(last_activity).to_datetime) : '', total_duration, hourly_rate ? hourly_rate[:hourlyRate].to_s + " #{hourly_rate[:currency]}" : '', hourly_rate ? (hourly_rate[:hourlyRate] * total / 3600.0).round(2).to_s + " #{hourly_rate[:currency]}" : '' ].join('","') + '"' end
details()
click to toggle source
# File lib/punchcard.rb, line 85 def details project_exists_or_stop! find_or_make_file output = [] data = project_data data[0] = "#{data[0]} (#{running_status})" data.map do |line| points = line_to_time_points(line) unless points output << line + "\n" next end starttime = points[0] endtime = points[1] || timestamp output << duration(starttime, endtime) + "\t" + self.class.format_time(Time.at(starttime)) + ' - ' + self.class.format_time(Time.at(endtime)) end output << "========\n#{humanized_total}\t(total)" output.join("\n") end
project()
click to toggle source
# File lib/punchcard.rb, line 166 def project @project.strip end
project=(project_name)
click to toggle source
# File lib/punchcard.rb, line 157 def project=(project_name) @project = project_name if @project.end_with?('*') @wilcard_for_filename = '*' @project = @project.chomp('*') end @project.strip end
remove()
click to toggle source
# File lib/punchcard.rb, line 141 def remove if File.exist?(project_file) File.delete(project_file) "Deleted #{project_file}" end end
rename(new_project_name)
click to toggle source
# File lib/punchcard.rb, line 148 def rename(new_project_name) old_filename = project_filename data = project_data data[0] = new_project_name write_string_to_project_file! data.join("\n") self.project = new_project_name File.rename(old_filename, project_filename) && "#{old_filename} -> #{project_filename}" end
set(key, value)
click to toggle source
# File lib/punchcard.rb, line 170 def set(key, value) unless key =~ /^[a-zA-Z0-9]+$/ raise PunchCardError, "Key '#{key}' can only be alphanumeric" end @meta_data[key.to_sym] = value write_to_project_file! @meta_data end
start()
click to toggle source
# File lib/punchcard.rb, line 31 def start output = [] if start_time && !end_time output << "'#{title_or_project}' already started (#{humanized_total} total)" output << duration(start_time, timestamp).to_s else output << "'#{title_or_project}' started (#{humanized_total} total)" self.start_time = timestamp end output.join("\n") end
status()
click to toggle source
# File lib/punchcard.rb, line 76 def status project_exists_or_stop! find_or_make_file output = [] output << (title_or_project + " (#{running_status})\n") output << humanized_total output.join("\n") end
stop()
click to toggle source
# File lib/punchcard.rb, line 43 def stop output = [] if end_time output << "'#{title_or_project}' already stopped (#{humanized_total} total)" elsif start_time output << "'#{title_or_project}' stopped (#{humanized_total} total)" self.end_time = timestamp else output << 'Nothing to stop' end output.join("\n") end
title_and_project()
click to toggle source
# File lib/punchcard.rb, line 68 def title_and_project if title != project "#{title} [#{project}]" else project end end
title_or_project()
click to toggle source
# File lib/punchcard.rb, line 64 def title_or_project title || project end
toggle()
click to toggle source
# File lib/punchcard.rb, line 56 def toggle if active? stop else start end end
total(start_at: nil, end_at: nil)
click to toggle source
# File lib/punchcard.rb, line 180 def total(start_at: nil, end_at: nil) total = 0 project_data.map do |line| points = line_to_time_points(line) next unless points start_time = points[0] end_time = points[1] || timestamp next if time_range_is_excluded_by_filter?( start_at: start_at, end_at: end_at, start_time: start_time, end_time: end_time ) total += end_time - start_time end total end
Private Instance Methods
active?()
click to toggle source
# File lib/punchcard.rb, line 238 def active? running_status == 'running' end
append_new_line(line)
click to toggle source
# File lib/punchcard.rb, line 345 def append_new_line(line) open(project_file, 'a') { |f| f.puts("\n" + line.to_s.strip) } end
duration(start_time, end_time)
click to toggle source
# File lib/punchcard.rb, line 250 def duration(start_time, end_time) if start_time self.class.humanize_duration end_time - start_time else self.class.humanize_duration 0 end end
end_time()
click to toggle source
# File lib/punchcard.rb, line 270 def end_time time_points ? time_points[1] : nil end
end_time=(time)
click to toggle source
# File lib/punchcard.rb, line 266 def end_time=(time) replace_last_line "#{timestamp_to_time(start_time)} - #{timestamp_to_time(time)}" end
find_or_make_file()
click to toggle source
# File lib/punchcard.rb, line 367 def find_or_make_file write_string_to_project_file!(@project + "\n") unless project_exist? self.title ||= project_data.first end
find_or_make_settings_dir()
click to toggle source
# File lib/punchcard.rb, line 372 def find_or_make_settings_dir Dir.mkdir(SETTINGS_DIR) unless File.exist?(SETTINGS_DIR) end
hourly_rate()
click to toggle source
# File lib/punchcard.rb, line 224 def hourly_rate hourly_rate_found = @meta_data[:hourlyRate]&.match(HOURLY_RATE_PATTERN) return unless hourly_rate_found { hourlyRate: hourly_rate_found[1].to_f, currency: hourly_rate_found[2] ? hourly_rate_found[2].strip : '' } end
humanized_total()
click to toggle source
# File lib/punchcard.rb, line 246 def humanized_total self.class.humanize_duration total end
last_entry()
click to toggle source
# File lib/punchcard.rb, line 299 def last_entry project_data.last end
line_to_time_points(line)
click to toggle source
# File lib/punchcard.rb, line 278 def line_to_time_points(line) matches = line.match(TIME_POINT_PATTERN) time_points = matches ? [string_to_timestamp(matches[2]), string_to_timestamp(matches[4])] : nil if time_points&.reject(&:nil?)&.empty? nil else time_points end end
project_data()
click to toggle source
# File lib/punchcard.rb, line 331 def project_data File.open(project_file).each_line.map(&:strip) end
project_exist?()
click to toggle source
# File lib/punchcard.rb, line 363 def project_exist? File.exist?(project_file) end
project_exists_or_stop!()
click to toggle source
# File lib/punchcard.rb, line 234 def project_exists_or_stop! raise PunchCardError, "'#{@project}' does not exists" unless project_exist? end
project_file()
click to toggle source
# File lib/punchcard.rb, line 355 def project_file Dir[project_filename + @wilcard_for_filename].sort_by { |f| File.mtime(f) }.reverse.first || project_filename end
project_filename()
click to toggle source
# File lib/punchcard.rb, line 359 def project_filename SETTINGS_DIR + "/#{sanitize_filename(@project)}" end
read_project_data()
click to toggle source
# File lib/punchcard.rb, line 311 def read_project_data title = nil timestamps = [] i = 0 File.open(project_file, 'r').each_line do |line| line.strip! if i.zero? title = line elsif line.match(META_KEY_PATTERN) set line.match(META_KEY_PATTERN)[1], line.match(META_KEY_PATTERN)[2] elsif line.match(TIME_POINT_PATTERN) timestamps.push line end i += 1 end @project = File.basename(project_file) self.title = title timestamps end
replace_last_line(line)
click to toggle source
# File lib/punchcard.rb, line 349 def replace_last_line(line) data = project_data data[-1] = line write_string_to_project_file! data.join("\n") end
running_status()
click to toggle source
# File lib/punchcard.rb, line 242 def running_status start_time && !end_time ? 'running' : 'stopped' end
sanitize_filename(name)
click to toggle source
# File lib/punchcard.rb, line 376 def sanitize_filename(name) name.downcase.gsub(%r{(\\|/)}, '').gsub(/[^0-9a-z.\-]/, '_') end
start_time()
click to toggle source
# File lib/punchcard.rb, line 258 def start_time time_points ? time_points[0] : nil end
start_time=(time)
click to toggle source
# File lib/punchcard.rb, line 262 def start_time=(time) append_new_line timestamp_to_time(time) end
string_to_timestamp(str)
click to toggle source
# File lib/punchcard.rb, line 289 def string_to_timestamp(str) return str if str.nil? str.strip! # here some legacy… previous versions stored timestamp, # but now punched stores date-time strings for better readability. # So we have to convert timestamp and date-time format into timestamp str =~ /^\d+$/ ? str.to_i : (str =~ /^\d{4}\-\d/ ? Time.parse(str).to_i : nil) end
time_points()
click to toggle source
# File lib/punchcard.rb, line 274 def time_points line_to_time_points last_entry end
time_range_is_excluded_by_filter?(start_time:, end_time:, start_at: nil, end_at: nil)
click to toggle source
# File lib/punchcard.rb, line 380 def time_range_is_excluded_by_filter?(start_time:, end_time:, start_at: nil, end_at: nil) start_at && start_at.to_time.to_i >= start_time || end_at && end_at.to_time.to_i <= end_time end
timestamp()
click to toggle source
# File lib/punchcard.rb, line 303 def timestamp Time.now.to_i end
timestamp_to_time(timestamp)
click to toggle source
# File lib/punchcard.rb, line 307 def timestamp_to_time(timestamp) Time.at(timestamp) end
write_string_to_project_file!(string)
click to toggle source
# File lib/punchcard.rb, line 335 def write_string_to_project_file!(string) File.open(project_file, 'w') { |f| f.write(string) } end
write_to_project_file!()
click to toggle source
# File lib/punchcard.rb, line 339 def write_to_project_file! timestamps = project_data.select { |line| line.match(TIME_POINT_PATTERN) } meta_data_lines = @meta_data.map { |key, value| "#{key}: #{value}" } write_string_to_project_file! [@project, meta_data_lines.join("\n"), timestamps].reject(&:empty?).join("\n") end