wads 0.1.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.
data/lib/wads/app.rb ADDED
@@ -0,0 +1,199 @@
1
+ require 'gosu'
2
+ require_relative 'data_structures'
3
+ require_relative 'widgets'
4
+ require_relative 'version'
5
+
6
+ include Wads
7
+
8
+ class WadsSampleApp < Gosu::Window
9
+
10
+ STOCKS_DATA_FILE = "./data/NASDAQ.csv"
11
+ LOTTERY_DATA_FILE = "./data/Pick4_12_21_2020.txt"
12
+
13
+ def initialize
14
+ super(800, 600)
15
+ self.caption = "Wads Sample App"
16
+ @font = Gosu::Font.new(32)
17
+ @title_font = Gosu::Font.new(38)
18
+ @version_font = Gosu::Font.new(22)
19
+ @small_font = Gosu::Font.new(22)
20
+ @banner_image = Gosu::Image.new("./media/Banner.png")
21
+ @display_widget = nil
22
+ end
23
+
24
+ def parse_opts_and_run
25
+ # Make help the default output if no args are specified
26
+ if ARGV.length == 0
27
+ ARGV[0] = "-h"
28
+ end
29
+
30
+ opts = SampleAppCommand.new.parse.run
31
+ if opts[:stocks]
32
+ stats = process_stock_data
33
+ if opts[:gui]
34
+ @display_widget = SampleStocksDisplay.new(@small_font, stats)
35
+ show
36
+ else
37
+ stats.report(Date::DAYNAMES[1..5])
38
+ end
39
+
40
+ elsif opts[:lottery]
41
+ process_lottery_data
42
+ else
43
+ puts " "
44
+ puts "Select one of the following sample analysis options"
45
+ puts "-s Run sample stocks analysis"
46
+ puts "-l Run sample analysis of lottery numbers"
47
+ puts " "
48
+ exit
49
+ end
50
+
51
+ end
52
+
53
+ def update
54
+ # TODO
55
+ end
56
+
57
+ def draw
58
+ draw_banner
59
+ @display_widget.draw
60
+ end
61
+
62
+ def draw_banner
63
+ @banner_image.draw(1,1,1,0.9,0.9)
64
+ @title_font.draw_text("Wads Sample App", 10, 20, 2, 1, 1, Gosu::Color::WHITE)
65
+ @version_font.draw_text("Version #{Wads::VERSION}", 13, 54, 2, 1, 1, Gosu::Color::WHITE)
66
+ end
67
+
68
+ def button_down id
69
+ close if id == Gosu::KbEscape
70
+ # Delegate button events to the primary display widget
71
+ result = @display_widget.button_down id, mouse_x, mouse_y
72
+ if result.close_widget
73
+ close
74
+ end
75
+ end
76
+
77
+ def process_stock_data
78
+ # The data file comes from https://finance.yahoo.com
79
+ # The format of this file is as follows:
80
+ #
81
+ # Date,Open,High,Low,Close,Adj Close,Volume
82
+ # 2000-01-03,4186.189941,4192.189941,3989.709961,4131.149902,4131.149902,1510070000
83
+ # 2000-01-04,4020.000000,4073.250000,3898.229980,3901.689941,3901.689941,1511840000
84
+ # ...
85
+ # 2020-12-30,12906.509766,12924.929688,12857.759766,12870.000000,12870.000000,5292210000
86
+ # 2020-12-31,12877.089844,12902.070313,12821.230469,12888.280273,12888.280273,4771390000
87
+
88
+ stats = Stats.new("NASDAQ")
89
+ previous_close = nil
90
+
91
+ puts "Read the data file #{STOCKS_DATA_FILE}"
92
+ File.readlines(STOCKS_DATA_FILE).each do |line|
93
+ line = line.chomp # remove the carriage return
94
+
95
+ # Ignore header and any empty lines, process numeric data lines
96
+ if line.length > 0 and line[0].match(/[0-9]/)
97
+ values = line.split(",")
98
+ date = Date.strptime(values[0], "%Y-%m-%d")
99
+ weekday = Date::DAYNAMES[date.wday]
100
+
101
+ open_value = values[1].to_f
102
+ close_value = values[4].to_f
103
+
104
+ if previous_close.nil?
105
+ # Just use the first day to set the baseline
106
+ previous_close = close_value
107
+ else
108
+ change_from_previous_close = close_value - previous_close
109
+ change_intraday = close_value - open_value
110
+ change_overnight = open_value - previous_close
111
+
112
+ change_percent = change_from_previous_close / previous_close
113
+
114
+ stats.add(weekday, change_percent)
115
+ stats.add("#{weekday} prev close", change_from_previous_close)
116
+ stats.add("#{weekday} intraday", change_intraday)
117
+ stats.add("#{weekday} overnight", change_overnight)
118
+
119
+ previous_close = close_value
120
+ end
121
+ end
122
+ end
123
+
124
+ stats
125
+ end
126
+
127
+ def process_lottery_data
128
+ puts "The lottery example has not been implemented yet"
129
+ puts "however the data is available in the data directory,"
130
+ puts "and the same pattern used in the stocks example can"
131
+ puts "be applied here."
132
+ end
133
+ end
134
+
135
+ class SampleStocksDisplay < Widget
136
+ attr_accessor :stats
137
+
138
+ def initialize(font, stats)
139
+ super(10, 100, COLOR_HEADER_BRIGHT_BLUE)
140
+ set_dimensions(780, 500)
141
+ set_font(font)
142
+ add_child(Document.new(sample_content, x + 5, y + 5, @width, @height, @font))
143
+ @exit_button = Button.new("Exit", 380, bottom_edge - 30, @font)
144
+ add_child(@exit_button)
145
+
146
+ @stats = stats
147
+ @data_table = SingleSelectTable.new(@x + 5, @y + 100, # top left corner
148
+ 770, 200, # width, height
149
+ ["Day", "Min", "Avg", "StdDev", "Max", "p10", "p90"], # column headers
150
+ @font, COLOR_WHITE) # font and text color
151
+ @data_table.selected_color = COLOR_LIGHT_GRAY
152
+ Date::DAYNAMES[1..5].each do |day|
153
+ min = format_percent(@stats.min(day))
154
+ avg = format_percent(@stats.average(day))
155
+ std = format_percent(@stats.std_dev(day))
156
+ max = format_percent(@stats.max(day))
157
+ p10 = format_percent(@stats.percentile(day, 0.1))
158
+ p90 = format_percent(@stats.percentile(day, 0.90))
159
+ @data_table.add_row([day, min, avg, std, max, p10, p90], COLOR_HEADER_BRIGHT_BLUE)
160
+ end
161
+ add_child(@data_table)
162
+ @selection_text = nil
163
+ end
164
+
165
+ def format_percent(val)
166
+ "#{(val * 100).round(3)}%"
167
+ end
168
+
169
+ def sample_content
170
+ <<~HEREDOC
171
+ This sample stock analysis uses NASDAQ data from https://finance.yahoo.com looking
172
+ at closing data through the years 2000 to 2020. The percent gain or loss is broken
173
+ down per day, as shown in the table below.
174
+ HEREDOC
175
+ end
176
+
177
+ def render
178
+ if @selection_text
179
+ @selection_text.draw
180
+ end
181
+ end
182
+
183
+ def button_down id, mouse_x, mouse_y
184
+ if id == Gosu::MsLeft
185
+ if @exit_button.contains_click(mouse_x, mouse_y)
186
+ return WidgetResult.new(true)
187
+ elsif @data_table.contains_click(mouse_x, mouse_y)
188
+ val = @data_table.set_selected_row(mouse_y, 0)
189
+ if val.nil?
190
+ # nothing to do
191
+ else
192
+ @selection_text = Text.new("You selected #{val}, a great day!",
193
+ x + 5, y + 400, @font)
194
+ end
195
+ end
196
+ end
197
+ WidgetResult.new(false)
198
+ end
199
+ end
@@ -0,0 +1,251 @@
1
+ require 'date'
2
+
3
+ module Wads
4
+
5
+ SPACER = " "
6
+ VALUE_WIDTH = 10
7
+ NODE_UNKNOWN = "undefined"
8
+
9
+ class HashOfHashes
10
+ attr_accessor :data
11
+
12
+ def initialize
13
+ @data = {}
14
+ end
15
+
16
+ def set(data_set_name, x, y)
17
+ data_set = @data[x]
18
+ if data_set.nil?
19
+ data_set = {}
20
+ @data[x] = data_set
21
+ end
22
+ data_set[x] = y
23
+ end
24
+
25
+ def get(data_set_name, x)
26
+ data_set = @data[x]
27
+ if data_set.nil?
28
+ return nil
29
+ end
30
+ data_set[x]
31
+ end
32
+ end
33
+
34
+ class Stats
35
+ attr_accessor :name
36
+ attr_accessor :data
37
+
38
+ def initialize(name)
39
+ @name = name
40
+ @data = {}
41
+ end
42
+
43
+ def add(key, value)
44
+ data_set = @data[key]
45
+ if data_set
46
+ data_set << value
47
+ else
48
+ data_set = []
49
+ data_set << value
50
+ @data[key] = data_set
51
+ end
52
+ end
53
+
54
+ def increment(key)
55
+ add(key, 1)
56
+ end
57
+
58
+ def count(key)
59
+ data_set = @data[key]
60
+ if data_set
61
+ return data_set.size
62
+ else
63
+ return 0
64
+ end
65
+ end
66
+
67
+ def sum(key)
68
+ data_set = @data[key]
69
+ if data_set
70
+ return data_set.inject(0.to_f){|sum,x| sum + x }
71
+ else
72
+ return 0
73
+ end
74
+ end
75
+
76
+ def average(key)
77
+ data_set = @data[key]
78
+ if data_set
79
+ return (sum(key) / count(key)).round(5)
80
+ else
81
+ return 0
82
+ end
83
+ end
84
+
85
+ def min(key)
86
+ data_set = @data[key]
87
+ if data_set
88
+ return data_set.min
89
+ else
90
+ return 0
91
+ end
92
+ end
93
+
94
+ def max(key)
95
+ data_set = @data[key]
96
+ if data_set
97
+ return data_set.max
98
+ else
99
+ return 0
100
+ end
101
+ end
102
+
103
+ def sample_variance(key)
104
+ data_set = @data[key]
105
+ return 0 unless data_set
106
+ m = average(key)
107
+ s = data_set.inject(0.0){|accum, i| accum +(i-m)**2 }
108
+ s/(data_set.length - 1).to_f
109
+ end
110
+
111
+ def std_dev(key)
112
+ Math.sqrt(sample_variance(key))
113
+ end
114
+
115
+ def halfway(b, e)
116
+ d = e - b
117
+ m = b + (d / 2).to_i
118
+ m
119
+ end
120
+
121
+ def percentile(key, pct)
122
+ data_set = @data[key]
123
+ if data_set
124
+ sorted_data_set = data_set.sort
125
+ pct_index = (data_set.length - 1).to_f * pct
126
+ mod = pct_index.modulo(1.0)
127
+ adj = pct_index
128
+ if mod > 0.9
129
+ adj = pct_index.ceil
130
+ elsif mod < 0.1
131
+ adj = pct_index.floor
132
+ else
133
+ # We want halfway between the two indices
134
+ low = pct_index.floor
135
+ high = pct_index.ceil
136
+ if low < 0
137
+ low = 0
138
+ end
139
+ if high > data_set.size - 1
140
+ high = data_set.size - 1
141
+ end
142
+ result = halfway(sorted_data_set[low], sorted_data_set[high])
143
+ return result
144
+ end
145
+
146
+ if adj < 0
147
+ adj = 0
148
+ elsif adj > data_set.size - 1
149
+ adj = data_set.size - 1
150
+ end
151
+ result = sorted_data_set[adj]
152
+ return result
153
+ else
154
+ return 0
155
+ end
156
+ end
157
+
158
+ def most_common(key)
159
+ value_counts = {}
160
+ data_set = @data[key]
161
+ data_set.each do |data|
162
+ c = value_counts[data]
163
+ if c.nil?
164
+ value_counts[data] = 1
165
+ else
166
+ value_counts[data] = value_counts[data] + 1
167
+ end
168
+ end
169
+
170
+ largest_count = 0
171
+ largest_key = "none"
172
+ value_counts.keys.each do |key|
173
+ key_count = value_counts[key]
174
+ if key_count > largest_count
175
+ largest_count = key_count
176
+ largest_key = key
177
+ end
178
+ end
179
+ largest_key
180
+ end
181
+
182
+ def pad(str, size, left_align = false)
183
+ str = str.to_s
184
+ if left_align
185
+ str[0, size].ljust(size, ' ')
186
+ else
187
+ str[0, size].rjust(size, ' ')
188
+ end
189
+ end
190
+
191
+ def display_counts
192
+ puts "#{pad(@name, 20)} Value"
193
+ puts "#{'-' * 20} #{'-' * 10}"
194
+ @data.keys.each do |key|
195
+ #data_set = @data[key]
196
+ puts "#{pad(key, 20)} #{count(key)}"
197
+ end
198
+ end
199
+
200
+ def keys
201
+ @data.keys
202
+ end
203
+
204
+ def report(report_keys = keys)
205
+ puts "#{pad(@name, 10)}#{SPACER}#{pad('Count', 7)}#{SPACER}#{pad('Min', VALUE_WIDTH)}#{SPACER}#{pad('Avg', VALUE_WIDTH)}#{SPACER}#{pad('StdDev', VALUE_WIDTH)}#{SPACER}#{pad('Max', VALUE_WIDTH)}#{SPACER}| #{pad('p1', VALUE_WIDTH)}#{SPACER}#{pad('p10', VALUE_WIDTH)}#{SPACER}#{pad('p50', VALUE_WIDTH)}#{SPACER}#{pad('p90', VALUE_WIDTH)}#{SPACER}#{pad('p99', VALUE_WIDTH)}"
206
+ puts "#{'-' * 10}#{SPACER}#{'-' * 7}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}| #{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}"
207
+ if report_keys.nil?
208
+ report_keys = @data.keys
209
+ end
210
+ report_keys.each do |key|
211
+ data_set = @data[key]
212
+ m1 = min(key).round(5)
213
+ a = average(key).round(5)
214
+ sd = std_dev(key).round(5)
215
+ m2 = max(key).round(5)
216
+ p1 = percentile(key, 0.01).round(5)
217
+ p10 = percentile(key, 0.1).round(5)
218
+ p50 = percentile(key, 0.5).round(5)
219
+ p90 = percentile(key, 0.90).round(5)
220
+ p99 = percentile(key, 0.99).round(5)
221
+ puts "#{pad(key, 10)}#{SPACER}#{pad(count(key), 7)}#{SPACER}#{pad(m1, VALUE_WIDTH)}#{SPACER}#{pad(a, VALUE_WIDTH)}#{SPACER}#{pad(sd, VALUE_WIDTH)}#{SPACER}#{pad(m2, VALUE_WIDTH)}#{SPACER}| #{pad(p1, VALUE_WIDTH)}#{SPACER}#{pad(p10, VALUE_WIDTH)}#{SPACER}#{pad(p50, VALUE_WIDTH)}#{SPACER}#{pad(p90, VALUE_WIDTH)}#{SPACER}#{pad(p99, VALUE_WIDTH)}"
222
+ end
223
+
224
+ end
225
+ end
226
+
227
+ class Node
228
+ attr_accessor :x
229
+ attr_accessor :y
230
+ attr_accessor :name
231
+ attr_accessor :type
232
+ attr_accessor :inputs
233
+ attr_accessor :outputs
234
+ attr_accessor :visited
235
+
236
+ def initialize(name, type = NODE_UNKNOWN)
237
+ @name = name
238
+ @type = type
239
+ @inputs = []
240
+ @outputs = []
241
+ @visited = false
242
+ end
243
+
244
+ #
245
+ # TODO Visitor pattern and solution for detecting cyclic graphs
246
+ #
247
+ # when you visit, reset all the visited flags
248
+ # set it to true when you visit the node
249
+ # first check though if visited already true, if so, you have a cycle
250
+ end
251
+ end