lewt 0.5.12
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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +238 -0
- data/bin/lewt +10 -0
- data/lib/config/customers.yml +33 -0
- data/lib/config/enterprise.yml +54 -0
- data/lib/config/settings.yml +10 -0
- data/lib/config/templates/invoice.html.liquid +63 -0
- data/lib/config/templates/invoice.text.liquid +40 -0
- data/lib/config/templates/meta.html.liquid +0 -0
- data/lib/config/templates/meta.text.liquid +1 -0
- data/lib/config/templates/metastat.html.liquid +2 -0
- data/lib/config/templates/metastat.text.liquid +8 -0
- data/lib/config/templates/report.html.liquid +29 -0
- data/lib/config/templates/report.text.liquid +15 -0
- data/lib/config/templates/style.css +461 -0
- data/lib/extension.rb +158 -0
- data/lib/extensions/calendar-timekeeping/apple_extractor.rb +63 -0
- data/lib/extensions/calendar-timekeeping/calendar-timekeeping.rb +65 -0
- data/lib/extensions/calendar-timekeeping/extractor.rb +62 -0
- data/lib/extensions/calendar-timekeeping/gcal_extractor.rb +61 -0
- data/lib/extensions/calendar-timekeeping/ical_extractor.rb +52 -0
- data/lib/extensions/liquid-renderer.rb +106 -0
- data/lib/extensions/metastat/metamath.rb +108 -0
- data/lib/extensions/metastat/metastat.rb +161 -0
- data/lib/extensions/simple-expenses.rb +112 -0
- data/lib/extensions/simple-invoices.rb +93 -0
- data/lib/extensions/simple-milestones.rb +102 -0
- data/lib/extensions/simple-reports.rb +81 -0
- data/lib/extensions/store.rb +81 -0
- data/lib/lewt.rb +233 -0
- data/lib/lewt_book.rb +29 -0
- data/lib/lewt_ledger.rb +149 -0
- data/lib/lewtopts.rb +170 -0
- data/tests/LEWT Schedule.ics +614 -0
- data/tests/expenses.csv +1 -0
- data/tests/milestones.csv +1 -0
- data/tests/run_tests.rb +14 -0
- data/tests/tc_Billing.rb +29 -0
- data/tests/tc_CalExt.rb +44 -0
- data/tests/tc_Lewt.rb +37 -0
- data/tests/tc_LewtExtension.rb +31 -0
- data/tests/tc_LewtLedger.rb +38 -0
- data/tests/tc_LewtOpts.rb +26 -0
- metadata +158 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
load File.expand_path('../extractor.rb', __FILE__)
|
2
|
+
load File.expand_path('../gcal_extractor.rb', __FILE__)
|
3
|
+
load File.expand_path('../ical_extractor.rb', __FILE__)
|
4
|
+
load File.expand_path('../apple_extractor.rb', __FILE__)
|
5
|
+
|
6
|
+
|
7
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
8
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
9
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
module LEWT
|
14
|
+
|
15
|
+
# The Calander Timekeeping LEWT Extensions lets you extract timesheet data from your iCal, Google Calender, or OSX Calender
|
16
|
+
# sources. In the process it transforms the data into a LEWTBook ready for processing.
|
17
|
+
#
|
18
|
+
# In order for your calender events to be recognised as billable timesheet entries there are some naming conventions you must
|
19
|
+
# observe.
|
20
|
+
#
|
21
|
+
# ===Conventions:
|
22
|
+
# - The <tt>title</tt> of your event must contain a client name or alias reference in it.
|
23
|
+
# - The <tt>description</tt> of your event will be pulled into the LEWTBook description column.
|
24
|
+
include CalendarExtractors
|
25
|
+
|
26
|
+
class CalendarTimekeeping < LEWT::Extension
|
27
|
+
|
28
|
+
# Sets up this extension and registers its options.
|
29
|
+
def initialize
|
30
|
+
# set extension options
|
31
|
+
options = {
|
32
|
+
:calendar => {
|
33
|
+
:default => "ical",
|
34
|
+
:definition => "The calender extraction method to use, supports 'gcal', 'ical', 'osx' calender extraction. Defaults to ical.",
|
35
|
+
:type => String,
|
36
|
+
},
|
37
|
+
:suppress => {
|
38
|
+
:definition => "Suppresses the cost calculation for the specified targets when calulating the hourly rates on extracted calender data.",
|
39
|
+
:type => String
|
40
|
+
}
|
41
|
+
}
|
42
|
+
super({:cmd => "calendar", :options => options})
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extracts data from a given calender source based on what was passed in the <tt>options['ext_method']</tt> parameter.
|
46
|
+
# options [Hash]:: The options hash passed to this function by the Lewt program.
|
47
|
+
# returns:: LEWTBook
|
48
|
+
def extract( options )
|
49
|
+
targetCustomers = self.get_matched_customers( options[:target], options[:suppress] )
|
50
|
+
dStart = options[:start]
|
51
|
+
dEnd = options[:end]
|
52
|
+
suppressTargets = options[:suppress] == nil ? nil : self.get_matched_customers(options[:suppress])
|
53
|
+
if options[:calendar] == "ical"
|
54
|
+
extract = CalendarExtractors::ICalExtractor.new( dStart, dEnd, targetCustomers, lewt_settings, suppressTargets )
|
55
|
+
elsif options[:calendar] == "gcal"
|
56
|
+
extract = CalendarExtractors::GCalExtractor.new(dStart, dEnd, targetCustomers, lewt_settings, suppressTargets )
|
57
|
+
elsif options[:calendar] == "osx"
|
58
|
+
extract = CalendarExtractors::AppleExtractor.new(dStart, dEnd, targetCustomers, lewt_settings, suppressTargets )
|
59
|
+
end
|
60
|
+
return extract.data
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
2
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
3
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
module CalendarExtractors
|
8
|
+
|
9
|
+
# The CalExtractor class acts as base class for various calender extraction interfaces such as GCalExtractor, ICalExtractor
|
10
|
+
# AppleExtractor. It provides some convenience methods that are useful across the various implementations.
|
11
|
+
class CalExtractor < LEWT::Extension
|
12
|
+
|
13
|
+
attr_reader :data
|
14
|
+
|
15
|
+
# Initialises this class. This method should be invoked by sub-classes with <tt>super()</tt>. It invokes
|
16
|
+
# <tt>extractCalenderData</tt> on the sub-classes behalf when called...
|
17
|
+
# dateStart [String]:: a human readable date as a string for the start time period
|
18
|
+
# dateEnd [String]:: a human readable date as a string for the end time period
|
19
|
+
# targets [Hash]:: a hash containing all the targets returned by the LewtExtension.get_matched_customers() method
|
20
|
+
def initialize( dateStart, dateEnd, targets )
|
21
|
+
@data = LEWT::LEWTBook.new
|
22
|
+
@dateStart = DateTime.parse dateStart.to_s
|
23
|
+
@dateEnd = DateTime.parse dateEnd.to_s
|
24
|
+
@targets = targets
|
25
|
+
@category = "Hourly Income"
|
26
|
+
self.extractCalendarData
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the extracted calendar data. Must be implimented by subclasses.
|
30
|
+
def extractCalendarData
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# Matches a search string against customer names/aliases
|
35
|
+
# evtSearch [String]:: a string to search against such as the title of an event
|
36
|
+
# returns:: false when no match found or the target customer details (as a hash) when matched
|
37
|
+
def isTargetCustomer? ( evtSearch )
|
38
|
+
match = false
|
39
|
+
@targets.each do |t|
|
40
|
+
reg = [ t['alias'], t['name'] ]
|
41
|
+
regex = Regexp.new( reg.join("|"), Regexp::IGNORECASE )
|
42
|
+
match = regex.match(evtSearch) != nil ? t : false;
|
43
|
+
break if match != false
|
44
|
+
end
|
45
|
+
return match
|
46
|
+
end
|
47
|
+
|
48
|
+
# Checks whether an event date is within target range
|
49
|
+
# date [Date]:: the date to check against
|
50
|
+
# return:: Boolean true/false operation status
|
51
|
+
def isTargetDate? ( date )
|
52
|
+
d = DateTime.parse(date.to_s)
|
53
|
+
check = false
|
54
|
+
if d >= @dateStart && d <= @dateEnd
|
55
|
+
check = true
|
56
|
+
end
|
57
|
+
return check
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'google_calendar'
|
2
|
+
|
3
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
4
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
5
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
6
|
+
|
7
|
+
|
8
|
+
module CalendarExtractors
|
9
|
+
|
10
|
+
# Extracts data from a Google Calender source.
|
11
|
+
#
|
12
|
+
# === Setup
|
13
|
+
# It is required that you have a Google App setup with your account to allow for API access to your data. Then
|
14
|
+
# add the following keys to your settings file.
|
15
|
+
#
|
16
|
+
# gmail_username:: The username for your google application.
|
17
|
+
# gmail_password:: The password associated with this username.
|
18
|
+
# gmail_app_name:: The name of the application you created.
|
19
|
+
#
|
20
|
+
class GCalExtractor < CalExtractor
|
21
|
+
|
22
|
+
# Sets up this extension
|
23
|
+
def initialize ( dStart, dEnd, targetCustomers, lewt_settings, suppressTargets )
|
24
|
+
uname = lewt_settings["gmail_username"]
|
25
|
+
pass = lewt_settings["gmail_password"]
|
26
|
+
app = lewt_settings["google_app_name"]
|
27
|
+
@googleCalender = Google::Calendar.new(
|
28
|
+
:username => uname,
|
29
|
+
:password => pass,
|
30
|
+
:app_name => app
|
31
|
+
)
|
32
|
+
super( dStart, dEnd, targetCustomers )
|
33
|
+
end
|
34
|
+
|
35
|
+
# This method does the actual google calender extract, comparing events to the requested paramters.
|
36
|
+
# It manipulates the @data property of this object which is used by LEWT to gather the extracted data.
|
37
|
+
def extractCalendarData
|
38
|
+
gConf = { :max_results => 2500, :order_by => 'starttime', :single_events => true }
|
39
|
+
@googleCalender.find_events_in_range(@dateStart, @dateEnd + 1, gConf).each do |e|
|
40
|
+
eStart = Time.parse( e.start_time )
|
41
|
+
eEnd = Time.parse( e.end_time )
|
42
|
+
timeDiff = (eEnd - eStart)/60/60
|
43
|
+
target = self.isTargetCustomer?(e.title)
|
44
|
+
if self.isTargetDate?( eStart ) == true && target != false
|
45
|
+
row = LEWT::LEWTLedger.new({
|
46
|
+
:date_start => eStart,
|
47
|
+
:date_end => eEnd,
|
48
|
+
:category => @category,
|
49
|
+
:entity => target["name"],
|
50
|
+
:description => e.content,
|
51
|
+
:quantity => timeDiff,
|
52
|
+
:unit_cost => target["rate"]
|
53
|
+
})
|
54
|
+
@data.push(row)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'icalendar'
|
2
|
+
|
3
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
4
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
5
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
6
|
+
|
7
|
+
|
8
|
+
module CalendarExtractors
|
9
|
+
|
10
|
+
# This class handles extraction from iCal sources.
|
11
|
+
#
|
12
|
+
# ===Usage:
|
13
|
+
# - add the key <tt>ical_filepath</tt> to you settings file corresponding to the filepath of the ical file you wish to have parsed.
|
14
|
+
#
|
15
|
+
class ICalExtractor < CalExtractor
|
16
|
+
|
17
|
+
# Initialises the object and calls the parent class' super() method.
|
18
|
+
def initialize( dateStart, dateEnd, targets, lewt_settings, suppressTargets )
|
19
|
+
@calendarPath = lewt_settings["ical_filepath"]
|
20
|
+
super( dateStart, dateEnd, targets )
|
21
|
+
end
|
22
|
+
|
23
|
+
# Open iCalender file, parses it, then check events with the regular CalExtractor methods.
|
24
|
+
# Sets the data property of this object if match data is found.
|
25
|
+
def extractCalendarData
|
26
|
+
calendars = Icalendar.parse( File.open( @calendarPath ) )
|
27
|
+
calendars.each do |calendar|
|
28
|
+
calendar.events.each do |e|
|
29
|
+
target = self.isTargetCustomer?( e.summary )
|
30
|
+
dstart = Time.parse( e.dtstart.to_s )
|
31
|
+
dend = Time.parse( e.dtend.to_s )
|
32
|
+
if self.isTargetDate?(dstart) == true && target != false
|
33
|
+
timeDiff = (dend - dstart) /60/60
|
34
|
+
row = LEWT::LEWTLedger.new({
|
35
|
+
:date_start => dstart,
|
36
|
+
:date_end => dend,
|
37
|
+
:category => @category,
|
38
|
+
:entity => target["name"],
|
39
|
+
:description => e.description.to_s,
|
40
|
+
:quantity => timeDiff,
|
41
|
+
:unit_cost => target["rate"]
|
42
|
+
})
|
43
|
+
|
44
|
+
@data.push( row )
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require "liquid"
|
2
|
+
require "pdfkit"
|
3
|
+
|
4
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
5
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
6
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
7
|
+
|
8
|
+
|
9
|
+
module LEWT
|
10
|
+
|
11
|
+
# The Liquid Renderer LEWT Extension handles rendering processed data to TEXT, HTML, and PDF formats using the
|
12
|
+
# {liquid templating engine}[http://liquidmarkup.org] at its core. This allows for easy marking up of templates
|
13
|
+
# to be used with arbitrary LEWT extensions and processing them into multiple human readable formats on the fly.
|
14
|
+
|
15
|
+
class LiquidRenderer < LEWT::Extension
|
16
|
+
|
17
|
+
attr_reader :textTemplate, :htmlTemplate, :pdfTemplate, :stylesheet, :markup
|
18
|
+
|
19
|
+
# Sets up this extension and registers its run-time options.
|
20
|
+
def initialize ()
|
21
|
+
options = {
|
22
|
+
:method => {
|
23
|
+
:definition => "Specify html, text, pdf, or any combination of the three to define output method",
|
24
|
+
:default => "text",
|
25
|
+
:short_flag => "-m",
|
26
|
+
:type => String
|
27
|
+
},
|
28
|
+
:save_path => {
|
29
|
+
:definition => "Specify where to save the output file (required for PDFs)",
|
30
|
+
:type => String
|
31
|
+
},
|
32
|
+
:liquid_template => {
|
33
|
+
:definition => "Override the template that liquid render should use. Defaults to the template which matches the processor name but you will want to override this if you are using multiple processors.",
|
34
|
+
:type => String
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
super({:cmd => "liquid_render", :options => options })
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Loads the plaint-text, html, & (optionally) pdf template files of the given template name and parses it with the Liquid class
|
43
|
+
# template [String]:: The name of the template to load.
|
44
|
+
def load_templates ( template )
|
45
|
+
@textTemplate = Liquid::Template::parse( File.open( File.expand_path( lewt_stash + "/templates/#{template}.text.liquid", __FILE__) ).read )
|
46
|
+
@htmlTemplate = Liquid::Template::parse( File.open( File.expand_path( lewt_stash + "/templates/#{template}.html.liquid", __FILE__) ).read )
|
47
|
+
@stylesheet = File.expand_path( lewt_stash + '/templates/style.css', __FILE__)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Called on LEWT render cycle, this method outputs the data as per a pre-formated liquid template.
|
51
|
+
# options [Hash]:: The options hash passed to this function by the Lewt program.
|
52
|
+
# data [Array]:: An array of hash data to format.
|
53
|
+
def render ( options, data )
|
54
|
+
output = Array.new
|
55
|
+
# template name is always the same as processor name
|
56
|
+
template = options[:liquid_template] != nil ? options[:liquid_template] : options[:process]
|
57
|
+
load_templates( template )
|
58
|
+
|
59
|
+
data.each_with_index do |d, i|
|
60
|
+
|
61
|
+
if options[:method].match "text"
|
62
|
+
r = textTemplate.render(d)
|
63
|
+
if options[:save_path]
|
64
|
+
save_name = format_save_name( options, i )
|
65
|
+
File.open( save_name, 'w') {|f| f.write r }
|
66
|
+
output << save_name
|
67
|
+
else
|
68
|
+
output << r
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if options[:method].match "html"
|
73
|
+
r = htmlTemplate.render(d)
|
74
|
+
if options[:save_path]
|
75
|
+
save_name = format_save_name( options, i )
|
76
|
+
File.open( save_name, 'w') {|f| f.write r }
|
77
|
+
output << save_name
|
78
|
+
else
|
79
|
+
output << r
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if options[:method].match "pdf"
|
84
|
+
raise ArgumentError,"--save-file flag must be specified for PDF output in #{self.class.name}" if !options[:save_path]
|
85
|
+
save_name = format_save_name( options, i )
|
86
|
+
html = htmlTemplate.render(d)
|
87
|
+
kit = PDFKit.new(html, :page_size => 'A4')
|
88
|
+
kit.stylesheets << @stylesheet
|
89
|
+
file = kit.to_file( save_name )
|
90
|
+
output << save_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# if options[:dump_output] != false
|
94
|
+
# output.each do |r|
|
95
|
+
# puts r
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
|
99
|
+
return output
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
3
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
4
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
5
|
+
|
6
|
+
|
7
|
+
module LEWT
|
8
|
+
|
9
|
+
# MetaMath is a base class for developing statistics routines for use with the Metastat LEWT extension
|
10
|
+
# It provides some convienience methods for developing new routines, as well as acting as a container
|
11
|
+
# for common mathematical proceedures.
|
12
|
+
|
13
|
+
class MetaMath
|
14
|
+
|
15
|
+
# Takes an array of numeric values and returns there mean.
|
16
|
+
# ar:: Array of values
|
17
|
+
def mean(ar)
|
18
|
+
raise TypeError "Expected an array" if not ar.kind_of?(Array)
|
19
|
+
total = ar.reduce(0) { |sum, x| x + sum }
|
20
|
+
return Float(total)/Float(ar.length)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Takes an array of values and returns there mode.
|
24
|
+
# ar:: Array of values
|
25
|
+
def mode(ar)
|
26
|
+
raise TypeError "Expected an array" if not ar.kind_of?(Array)
|
27
|
+
freq = ar.inject(Hash.new(0)) { |h,v| h[v] += 1; h }
|
28
|
+
return ar.max_by { |v| freq[v] }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Takes an array of values and returns the median
|
32
|
+
# ar:: Array of values
|
33
|
+
def median(ar)
|
34
|
+
raise TypeError "Expected an array" if not ar.kind_of?(Array)
|
35
|
+
mid = ar.length / 2
|
36
|
+
return (mid % 1 != 0) ? mean( [ ar[(mid).floor], ar[(mid).round ]] ) : ar[mid]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the descriptive statistics for an array of values [mean, media, mode]
|
40
|
+
# ar:: Array of values
|
41
|
+
def descriptive_stats(ar)
|
42
|
+
raise TypeError "Expected an array" if not ar.kind_of?(Array)
|
43
|
+
return {
|
44
|
+
:mean => mean(ar),
|
45
|
+
:median => median(ar),
|
46
|
+
:mode => mode(ar)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# This subclass performs a Pearson Correlation analysis on an x/y dataset.
|
53
|
+
class PearsonR < MetaMath
|
54
|
+
|
55
|
+
def initialize (xs, ys)
|
56
|
+
raise Exception "_x & _y datasets must be of equal length" if xs.length != ys.length
|
57
|
+
@xs, @ys = xs, ys
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a Pearson Correlation R value
|
61
|
+
# xs:: The x series array of values
|
62
|
+
# ys:: The y series array of values
|
63
|
+
def correlate(xs = @xs, ys = @ys)
|
64
|
+
raise Exception "_x & _y datasets must be of equal length" if xs.length != ys.length
|
65
|
+
x_mean = mean(@xs)
|
66
|
+
y_mean = mean(@ys)
|
67
|
+
numerator = (0...@xs.length).reduce(0) do |sum, i|
|
68
|
+
sum + ((@xs[i] - x_mean) * (@ys[i] - y_mean))
|
69
|
+
end
|
70
|
+
denominator = @xs.reduce(0) do |sum, x|
|
71
|
+
sum + ((x - x_mean) ** 2)
|
72
|
+
end
|
73
|
+
(numerator / Math.sqrt(denominator))
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# This class performas a simple regression on an x/y dataset
|
80
|
+
class SimpleRegression < MetaMath
|
81
|
+
|
82
|
+
def initialize (xs, ys)
|
83
|
+
raise Exception "_x & _y datasets must be of equal length" if xs.length != ys.length
|
84
|
+
@xs, @ys = xs, ys
|
85
|
+
end
|
86
|
+
|
87
|
+
def y_intercept
|
88
|
+
mean(@ys) - (slope * mean(@xs))
|
89
|
+
end
|
90
|
+
|
91
|
+
def slope
|
92
|
+
x_mean = mean(@xs)
|
93
|
+
y_mean = mean(@ys)
|
94
|
+
|
95
|
+
numerator = (0...@xs.length).reduce(0) do |sum, i|
|
96
|
+
sum + ((@xs[i] - x_mean) * (@ys[i] - y_mean))
|
97
|
+
end
|
98
|
+
|
99
|
+
denominator = @xs.reduce(0) do |sum, x|
|
100
|
+
sum + ((x - x_mean) ** 2)
|
101
|
+
end
|
102
|
+
|
103
|
+
(numerator / denominator)
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require_relative "metamath.rb"
|
2
|
+
|
3
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
4
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
5
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
6
|
+
|
7
|
+
|
8
|
+
module LEWT
|
9
|
+
|
10
|
+
# The Metastat extension [experimental] handles processing meta-data added to ledger entries. It can compute statistics based of
|
11
|
+
# these parsed values
|
12
|
+
#
|
13
|
+
# ===Usage:
|
14
|
+
# Here is an example 'description' field entered into a LEWTLedger which can be parsed with Metastat
|
15
|
+
#
|
16
|
+
# 'description' => 'Adding some features to the system #happiness=10 #average-pay'
|
17
|
+
#
|
18
|
+
# Metastat will extract the # tags from the above string and log an indicatior called happinesswhich is equal to 10
|
19
|
+
# and a Boolean switch average-pay will be set to true
|
20
|
+
#
|
21
|
+
# These values will be aggregate over the whole 'extract' dataset then processed in a few ways.
|
22
|
+
#
|
23
|
+
# 1. Create a graph out of the data
|
24
|
+
# 2. Create a summary table out of the data
|
25
|
+
|
26
|
+
class Metastat < LEWT::Extension
|
27
|
+
|
28
|
+
attr_reader :client_table, :client_graph
|
29
|
+
|
30
|
+
# Sets up this extension and registers its options
|
31
|
+
def initialize
|
32
|
+
options = {
|
33
|
+
:tags => {
|
34
|
+
:definition => "A comma seperated list of metatags to lookup.",
|
35
|
+
:type => String
|
36
|
+
},
|
37
|
+
:y_series => {
|
38
|
+
:definition => "An optional y series to use for a correlation analysis.",
|
39
|
+
:type => String
|
40
|
+
}
|
41
|
+
}
|
42
|
+
@boolean_table = Hash.new
|
43
|
+
@raw_data = Hash.new
|
44
|
+
@dataset = Hash.new
|
45
|
+
super({:cmd => 'metastat', :options => options})
|
46
|
+
end
|
47
|
+
|
48
|
+
# Handles the process event for this extension
|
49
|
+
# options [Hash]:: The options hash passed to this function by the Lewt program.
|
50
|
+
# extract_data [LEWTBook]:: The data in LEWTBook format
|
51
|
+
def process (options, extract_data)
|
52
|
+
@options = options
|
53
|
+
extract_data.each do |row|
|
54
|
+
filter_row_values(row, options)
|
55
|
+
end
|
56
|
+
@dataset["frequency_table"] = @boolean_table
|
57
|
+
if options[:y_series]
|
58
|
+
@dataset["correlations"] = compute_correlations @raw_data
|
59
|
+
end
|
60
|
+
return @dataset
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
# extracts row data from a LEWTLedger object. Also handles initiating tag lookup.
|
66
|
+
# row [LEWTLedger]:: a LEWTLedger object container the row data.
|
67
|
+
# options [Hash]:: a Hash containing the options passed to LEWT.
|
68
|
+
def filter_row_values (row, options)
|
69
|
+
return if row.metatags == nil
|
70
|
+
# match tags requested via options
|
71
|
+
options[:tags].split(Lewt::OPTION_DELIMITER_REGEX).each do |meta|
|
72
|
+
row.metatags.each { |k, v|
|
73
|
+
next unless meta.gsub(Lewt::OPTION_SYMBOL_REGEX,"_").to_sym == k
|
74
|
+
# found match operate on it
|
75
|
+
client = get_matched_customers(row[:entity])[0]
|
76
|
+
# case our value
|
77
|
+
case v
|
78
|
+
when !!v == v
|
79
|
+
add_to_tally( client, row, k, v )
|
80
|
+
when Rational
|
81
|
+
transform_dataset( client, row, k, v)
|
82
|
+
end
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# adds +1 count to the client table for the provided key
|
88
|
+
# c [Hash]:: The client hash
|
89
|
+
# r [LEWTLedger]:: A LEWTLedger row
|
90
|
+
# k:: the metatag hash key
|
91
|
+
# v:: the meta tag value, this should be a boolean
|
92
|
+
def add_to_tally( c, r, k, v )
|
93
|
+
if !@boolean_table.has_key?(c["name"])
|
94
|
+
@boolean_table[ c["name"] ] = { k.to_s => 0 }
|
95
|
+
end
|
96
|
+
|
97
|
+
if !@boolean_table[c["name"]].has_key?(k.to_s)
|
98
|
+
@boolean_table[c["name"]][k.to_s] = 0;
|
99
|
+
end
|
100
|
+
|
101
|
+
@boolean_table[c["name"]][k.to_s] += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
# Transforms the dataset into something usable for statistical computation
|
105
|
+
# c [Hash]:: The client hash
|
106
|
+
# r [LEWTLedger]:: A LEWTLedger row
|
107
|
+
# k:: the metatag hash key
|
108
|
+
# v:: the meta tag value, this should be a boolean
|
109
|
+
def transform_dataset( c, r, k, v )
|
110
|
+
if !@raw_data.has_key?(c["name"])
|
111
|
+
@raw_data[ c["name"] ] = Array.new
|
112
|
+
end
|
113
|
+
# prep the data
|
114
|
+
@raw_data[c["name"]].push(prepare_row_data(r,k,v))
|
115
|
+
end
|
116
|
+
|
117
|
+
# prepares normal row for statistical analysis computing some basic numbers we are going to need
|
118
|
+
# r [LEWTLedger]:: A LEWTLedger row
|
119
|
+
# v:: the meta tag value, this should be a boolean
|
120
|
+
def prepare_row_data ( r, k, v )
|
121
|
+
data = {
|
122
|
+
k.to_sym => v.to_f,
|
123
|
+
:duration => r[:quantity].to_f,
|
124
|
+
:value => r[:total].to_f
|
125
|
+
}
|
126
|
+
return data
|
127
|
+
end
|
128
|
+
|
129
|
+
# performs some statistics using R.
|
130
|
+
# d:: the dataset to work with
|
131
|
+
def compute_correlations(d)
|
132
|
+
result = nil
|
133
|
+
d.each { |context, data_array|
|
134
|
+
# hash of arrays
|
135
|
+
data_hash = Hash.new
|
136
|
+
data_array.each do |r|
|
137
|
+
r.each { |k,v|
|
138
|
+
data_hash[k] = Array.new if !data_hash.has_key? k
|
139
|
+
data_hash[k].push v
|
140
|
+
}
|
141
|
+
end
|
142
|
+
result = correlate_y data_hash, @options[:y_series]
|
143
|
+
}
|
144
|
+
return result
|
145
|
+
end
|
146
|
+
|
147
|
+
def correlate_y(r_dataset, y_key)
|
148
|
+
results = Hash.new
|
149
|
+
r_dataset.each { |k, v_set|
|
150
|
+
next if k == y_key.to_sym
|
151
|
+
results[y_key.to_sym] = Hash.new if results[y_key.to_sym] == nil
|
152
|
+
results[y_key.to_sym][k] = Hash.new if results[y_key.to_sym][k] == nil
|
153
|
+
r = PearsonR.new( v_set, r_dataset[y_key.to_sym] )
|
154
|
+
results[y_key.to_sym][k][:pearson_r] = r.correlate
|
155
|
+
results[y_key.to_sym][k][:descriptive_stats] = r.descriptive_stats(v_set)
|
156
|
+
}
|
157
|
+
return results
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|