was 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35789d2f200b0436e67730bf7e2811cd61665f6bef985bc1f2cb091ed1797a5b
4
- data.tar.gz: efd553a27afd03bb2da2443d80945239b7e22487dac6e850982d498ec32d5d01
3
+ metadata.gz: 974fa97c47b4fe794512983c75fa065b9bdd40370e5987b1e4523d01791bdd2a
4
+ data.tar.gz: 9f6e783dd2fbad215e8c2b16c820a1b8642b843efba424ed2d35e5aa1e7e2b92
5
5
  SHA512:
6
- metadata.gz: 630fc202315d1b3d90c28214f97d23ba66f7f5daba3a43651cf720d21a7ece94c6d2533715267933edc2f6b18f662301d71d6590c7d9db655aa30dbcc90703d6
7
- data.tar.gz: 9e95ffaddcbae72fba82c4957b7ac20bb6db44bc219c6e13b445281799aafead6a9c722b0a7235975d737f877828da63dfc70e0284b65288e284e0245b54614a
6
+ metadata.gz: 43d1c61377ad1da1477d5edf911fd55b22acea765ffcf10e53c3cdc2c181e7968bd3bc02223721a82146c57b5186f79d25b75557026cedecb30dfc218a56e801
7
+ data.tar.gz: 6c09a8018b3d7fb5634985e3144d75adc942a21b3b6b459057192b1f0918bfee840b54a8897dc9ad25983623cbdf858b40c9e72f84ab073d3a891592998e5d41
data/README.md CHANGED
@@ -14,8 +14,14 @@ Scenario:
14
14
  * 'C' is 50%
15
15
  * 'D' is 0%
16
16
  * The practical is a simple mark out of 10.
17
- * 4 out of 10 is 40%
18
- * The person is given a final score out of 1000.
17
+ * 4 out of 10 is 40%
18
+ * The person is given a final score out of 1000.
19
+
20
+ ### Install the RubyGem
21
+
22
+ ```bash
23
+ gem install was
24
+ ```
19
25
 
20
26
  ### Define score classes
21
27
 
@@ -25,21 +31,30 @@ require "was"
25
31
  class ReportScore < WAS::Score
26
32
  maximum_score 1000
27
33
 
28
- with :exam, class_name: "ExamScore", weight: 0.75
29
- with :practical class_name: "PracticalScore", weight: 0.25
34
+ with :exam, class_name: "ExamScore", weight: 0.75
35
+ with :practical, class_name: "PracticalScore", weight: 0.25
30
36
  end
31
37
 
32
38
  class ExamScore < WAS::Score
33
- def calculation
34
- return 1 if input == "A"
35
- return 0.75 if input == "B"
36
- return 0.5 if input == "C"
39
+ context :grade_a, score: 1 do |input|
40
+ input == "A"
41
+ end
42
+
43
+ context :grade_b, score: 0.75 do |input|
44
+ input == "B"
45
+ end
46
+
47
+ context :grade_c, score: 0.5 do |input|
48
+ input == "C"
49
+ end
50
+
51
+ context :flunk do
37
52
  0
38
53
  end
39
54
  end
40
55
 
41
56
  class PracticalScore < WAS::Score
42
- def calculation
57
+ context :score do |input|
43
58
  input / 10.0
44
59
  end
45
60
  end
@@ -68,8 +83,8 @@ Omitting the `maximum_score` will return a composed percentage represented as a
68
83
  ```ruby
69
84
  # report_score.rb
70
85
  class ReportScore < WAS::Score
71
- with :exam, class_name: "ExamScore", weight: 0.75
72
- with :practical class_name: "PracticalScore", weight: 0.25
86
+ with :exam, class_name: "ExamScore", weight: 0.75
87
+ with :practical, class_name: "PracticalScore", weight: 0.25
73
88
  end
74
89
  ```
75
90
 
@@ -82,9 +97,80 @@ ReportScore.new({
82
97
  #> 0.875
83
98
  ```
84
99
 
100
+ ### Working with a score tree
101
+
102
+ For more complex scenarios, we might need to know more about how the score was composed.
103
+
104
+ Using the example `ReportScore`, `ExamScore`, and `PracticalScore` classes as
105
+ an example, we can generate a tree of scores:
106
+
107
+ ```ruby
108
+ tree = ReportScore.new({
109
+ exam: "A",
110
+ practical: 5
111
+ }).calculate(:tree)
112
+
113
+ #> tree
114
+ {
115
+ score: 875.0,
116
+ max: 1000,
117
+ deduction: -125.0,
118
+ with: {
119
+ exam: {
120
+ score: 750.0,
121
+ max: 750.0,
122
+ deduction: 0.0,
123
+ weight: 0.75
124
+ },
125
+ practical: {
126
+ score: 125.0,
127
+ max: 250.0,
128
+ deduction: -125.0,
129
+ weight: 0.25
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ This result is a `WAS::Tree` object. Basically a `Hash` with some extra
136
+ added to it.
137
+
138
+ #### Ordering by attribute
139
+
140
+ For each of the key attributes `:score`, `:max`, `:weight`, and
141
+ `:deduction`, we can order our tree interally by that key:
142
+
143
+ ```ruby
144
+ #> tree.order(:deduction)
145
+ {
146
+ score: 875.0,
147
+ max: 1000,
148
+ deduction: -125.0
149
+ with: {
150
+ practical: {
151
+ score: 125.0,
152
+ max: 250.0,
153
+ deduction: -125.0,
154
+ weight: 0.25
155
+ },
156
+ exam: {
157
+ score: 750.0,
158
+ max: 750.0,
159
+ deduction: 0.0,
160
+ weight: 0.75
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ For `:deduction`, results adjacent to each other are ordered by the
167
+ _most negative_. For all other options, the values are ordered largest
168
+ value first.
169
+
85
170
  ### View all weights
86
171
 
87
- If you want to see all of the weights that are used to compose the score, there is a convenience method `.weights`:
172
+ If you want to see all of the weights that are used to compose the score,
173
+ there is a convenience method `.weights`:
88
174
 
89
175
  ```ruby
90
176
  ReportScore.weights
data/lib/was/score.rb CHANGED
@@ -40,20 +40,62 @@ module WAS
40
40
  @input = input
41
41
  end
42
42
 
43
- def calculate
44
- calculation
43
+ def calculate(option = nil)
44
+ return calculation if option != :tree
45
+
46
+ calc = calculation(:tree)
47
+ tree = if calc.is_a?(Hash)
48
+ calc.merge(additional_score_attributes(calc[:score]))
49
+ else
50
+ {}.tap do |t|
51
+ t.merge!(score: calc)
52
+ t.merge!(additional_score_attributes(calc))
53
+ end
54
+ end
55
+
56
+ transform_scores_relative_to_max_score(tree)
45
57
  end
46
58
 
47
- def calculation
59
+ def calculation(option = nil)
48
60
  if contexts?
49
61
  context_score_calculation
50
62
  else
51
- nested_score_calcuation
63
+ nested_score_calcuation(option)
52
64
  end
53
65
  end
54
66
 
55
67
  private
56
68
 
69
+ def additional_score_attributes(score_value)
70
+ {
71
+ max: self.class.max_score,
72
+ deduction: (score_value - self.class.max_score).round(8)
73
+ }
74
+ end
75
+
76
+ def transform_scores_relative_to_max_score(tree, max_score = nil)
77
+ max_score = max_score || self.class.max_score
78
+ return tree if self.class.max_score == 1
79
+
80
+ tree.each do |key, value|
81
+ next if key != :with
82
+
83
+ value.each do |scorer, branch|
84
+ adjust_values_relative_to_max_score(branch, max_score)
85
+ end
86
+
87
+ value.transform_values! do |nested_tree|
88
+ transform_scores_relative_to_max_score(nested_tree, nested_tree[:max])
89
+ end
90
+ end
91
+ end
92
+
93
+ def adjust_values_relative_to_max_score(branch, max_score)
94
+ branch[:max] = branch[:max] * max_score * branch[:weight]
95
+ branch[:score] = branch[:score] * branch[:max]
96
+ branch[:deduction] = branch[:score] - branch[:max]
97
+ end
98
+
57
99
  def contexts?
58
100
  !!self.class.instance_variable_get("@contexts")
59
101
  end
@@ -61,16 +103,36 @@ module WAS
61
103
  def context_score_calculation
62
104
  self.class.instance_variable_get("@contexts").each do |context|
63
105
  output = context[:code].call(input)
64
- next unless output
106
+ next if !output
65
107
  return context[:score] || output
66
108
  end
67
109
  end
68
110
 
69
- def nested_score_calcuation
70
- self.class.scorers.sum do |name, scorer|
111
+ def nested_score_calcuation(option)
112
+ if option == :tree
113
+ WAS::Tree.new.tap do |t|
114
+ t[:score] = sum
115
+ t[:with] = with_attribute
116
+ end
117
+ else
118
+ sum
119
+ end
120
+ end
121
+
122
+ def sum
123
+ @sum ||= self.class.scorers.sum do |name, scorer|
71
124
  score = Object.const_get(scorer[:class_name]).new(input[name.to_sym]).calculate
72
125
  score * scorer[:weight]
73
126
  end * self.class.max_score
74
127
  end
128
+
129
+ def with_attribute
130
+ {}.tap do |with|
131
+ self.class.scorers.each do |name, scorer|
132
+ with[name] = Object.const_get(scorer[:class_name]).new(input[name.to_sym]).calculate(:tree)
133
+ with[name][:weight] = scorer[:weight]
134
+ end
135
+ end
136
+ end
75
137
  end
76
138
  end
data/lib/was/tree.rb ADDED
@@ -0,0 +1,31 @@
1
+ module WAS
2
+ class Tree < Hash
3
+ def order(order_key = :deduction)
4
+ self.dup.tap do |tree|
5
+ return tree if tree[:with].nil?
6
+
7
+ sort_with_by!(order_key, tree)
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def sort_with_by!(order_key, tree)
14
+ return tree if tree[:with].nil?
15
+
16
+ tree[:with].each do |_, subtree|
17
+ sort_with_by!(order_key, subtree)
18
+ end
19
+
20
+ array = tree[:with].sort_by do |_, subtree|
21
+ subtree[order_key]
22
+ end
23
+
24
+ tree[:with] = if order_key == :deduction
25
+ array.to_h
26
+ else
27
+ array.reverse.to_h
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/was/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WAS
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/was.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "was/version"
4
+ require_relative "was/tree"
4
5
  require_relative "was/score"
5
6
 
6
7
  module WAS
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: was
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Connell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-18 00:00:00.000000000 Z
11
+ date: 2025-02-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A simple gem/dsl for generating Weighted Average Score calculations.
14
14
  email:
@@ -23,6 +23,7 @@ files:
23
23
  - Rakefile
24
24
  - lib/was.rb
25
25
  - lib/was/score.rb
26
+ - lib/was/tree.rb
26
27
  - lib/was/version.rb
27
28
  - sig/was.rbs
28
29
  - was.gemspec