hash_math 0.0.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -4
- data/.ruby-version +1 -1
- data/.travis.yml +3 -4
- data/CHANGELOG.md +28 -0
- data/README.md +352 -1
- data/hash_math.gemspec +8 -5
- data/lib/hash_math.rb +9 -0
- data/lib/hash_math/mapper.rb +65 -0
- data/lib/hash_math/mapper/lookup.rb +56 -0
- data/lib/hash_math/mapper/mapping.rb +77 -0
- data/lib/hash_math/matrix.rb +60 -0
- data/lib/hash_math/matrix/key_value_pair.rb +38 -0
- data/lib/hash_math/record.rb +57 -0
- data/lib/hash_math/table.rb +61 -0
- data/lib/hash_math/unpivot.rb +54 -0
- data/lib/hash_math/unpivot/pivot.rb +47 -0
- data/lib/hash_math/unpivot/pivot_set.rb +54 -0
- data/lib/hash_math/version.rb +1 -1
- data/spec/hash_math/mapper/mapping_spec.rb +74 -0
- data/spec/hash_math/mapper_spec.rb +139 -0
- data/spec/hash_math/matrix_spec.rb +57 -0
- data/spec/hash_math/record_spec.rb +48 -0
- data/spec/hash_math/table_spec.rb +91 -0
- data/spec/hash_math/unpivot/pivot_spec.rb +53 -0
- data/spec/hash_math/unpivot_spec.rb +121 -0
- metadata +63 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a13830be433114e0438c34e2e15e136a01ab61c93b57c14e9be2b39a4dc9a7bf
|
4
|
+
data.tar.gz: 14a4d2765184d10f6149dd446be54f1cea68c505e308fbf884a982cecc0ad161
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5294dee4cb58c9d28cc829b2d18a51edb6036bf110c2ad94bc827e52d80e6b7417cf96e5b699af812b91d6b9ee9be75e2ba2f796a65234e333f68f3fb587033
|
7
|
+
data.tar.gz: '0706231920aa50e791e43820e9544a399d132ff9f5a1ee29cd0fc2c70414ecb181d28350a4aa3f1b5df67bdf904dc93b876246cfdb4e53507e33358e1100de71'
|
data/.rubocop.yml
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
-
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.5
|
3
|
+
NewCops: enable
|
4
|
+
|
5
|
+
Layout/LineLength:
|
2
6
|
Max: 100
|
3
7
|
|
4
8
|
Metrics/BlockLength:
|
@@ -13,9 +17,6 @@ Metrics/BlockLength:
|
|
13
17
|
Metrics/MethodLength:
|
14
18
|
Max: 25
|
15
19
|
|
16
|
-
AllCops:
|
17
|
-
TargetRubyVersion: 2.3
|
18
|
-
|
19
20
|
Metrics/AbcSize:
|
20
21
|
Max: 16
|
21
22
|
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.6.
|
1
|
+
2.6.6
|
data/.travis.yml
CHANGED
@@ -4,10 +4,9 @@ env:
|
|
4
4
|
language: ruby
|
5
5
|
rvm:
|
6
6
|
# Build on the latest stable of all supported Rubies (https://www.ruby-lang.org/en/downloads/):
|
7
|
-
- 2.
|
8
|
-
- 2.
|
9
|
-
- 2.
|
10
|
-
- 2.6.3
|
7
|
+
- 2.5.8
|
8
|
+
- 2.6.6
|
9
|
+
- 2.7.1
|
11
10
|
cache: bundler
|
12
11
|
before_script:
|
13
12
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
# 1.2.0 (August 19th, 2020)
|
2
|
+
|
3
|
+
Additions:
|
4
|
+
|
5
|
+
* HashMath::Mapper
|
6
|
+
|
7
|
+
# 1.1.0 (August 13th, 2020)
|
8
|
+
|
9
|
+
Additions:
|
10
|
+
|
11
|
+
* HashMath::Unpivot
|
12
|
+
|
13
|
+
Removals:
|
14
|
+
|
15
|
+
* Support for Ruby < 2.5
|
16
|
+
|
17
|
+
# 1.0.0 (September 18th, 2019)
|
18
|
+
|
19
|
+
Initial release.
|
20
|
+
|
21
|
+
# 1.0.0-alpha (September 16th, 2019)
|
22
|
+
|
23
|
+
Added initial implementation of:
|
24
|
+
|
25
|
+
* HashMath::Matrix
|
26
|
+
* HashMath::Record
|
27
|
+
* HashMath::Table
|
28
|
+
|
1
29
|
# 0.0.1 (September 16th, 2019)
|
2
30
|
|
3
31
|
Published initial shell. Library has no functionality yet.
|
data/README.md
CHANGED
@@ -2,4 +2,355 @@
|
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/hash_math.svg)](https://badge.fury.io/rb/hash_math) [![Build Status](https://travis-ci.org/bluemarblepayroll/hash_math.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/hash_math) [![Maintainability](https://api.codeclimate.com/v1/badges/9f9a504b3f5df199a253/maintainability)](https://codeclimate.com/github/bluemarblepayroll/hash_math/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/9f9a504b3f5df199a253/test_coverage)](https://codeclimate.com/github/bluemarblepayroll/hash_math/test_coverage) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
4
4
|
|
5
|
-
|
5
|
+
Ruby's Hash data structure is ubiquitous and highly flexible. This library contains common Hash patterns widely used within our code-base:
|
6
|
+
|
7
|
+
* Matrix: build a hash up then expand into hash products
|
8
|
+
* Record: define a list of keys and a base value (for each key) to use as a blueprint for hashes
|
9
|
+
* Table: key-value pair builder for constructing a two-dimensional array (rows by fields)
|
10
|
+
|
11
|
+
See examples for more information on each data structure.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
To install through Rubygems:
|
16
|
+
|
17
|
+
````bash
|
18
|
+
gem install install hash_math
|
19
|
+
````
|
20
|
+
|
21
|
+
You can also add this to your Gemfile:
|
22
|
+
|
23
|
+
````bash
|
24
|
+
bundle add hash_math
|
25
|
+
````
|
26
|
+
|
27
|
+
## Examples
|
28
|
+
|
29
|
+
### Matrix: The Hash Combination Calculator
|
30
|
+
|
31
|
+
HashMath::Matrix is a key-value builder that outputs all the different hash combinations.
|
32
|
+
|
33
|
+
Say we have this type of matrix:
|
34
|
+
|
35
|
+
````ruby
|
36
|
+
{
|
37
|
+
a: [1,2,3],
|
38
|
+
b: [4,5,6],
|
39
|
+
c: [7,8,9]
|
40
|
+
}
|
41
|
+
````
|
42
|
+
|
43
|
+
We could code this as:
|
44
|
+
|
45
|
+
````ruby
|
46
|
+
matrix = HashMath::Matrix.new
|
47
|
+
matrix.add_each(:a, [1,2,3])
|
48
|
+
matrix.add_each(:b, [4,5,6])
|
49
|
+
matrix.add_each(:c, [7,8,9])
|
50
|
+
````
|
51
|
+
|
52
|
+
Note: you can also call `Matrix#add` to only add a single value instead of add_each.
|
53
|
+
|
54
|
+
and get the combinations by calling to_a:
|
55
|
+
|
56
|
+
````ruby
|
57
|
+
combinations = matrix.to_a
|
58
|
+
````
|
59
|
+
|
60
|
+
which would yield the following hashes:
|
61
|
+
|
62
|
+
````ruby
|
63
|
+
[
|
64
|
+
{ a: 1, b: 4, c: 7 },
|
65
|
+
{ a: 1, b: 4, c: 8 },
|
66
|
+
{ a: 1, b: 4, c: 9 },
|
67
|
+
{ a: 1, b: 5, c: 7 },
|
68
|
+
{ a: 1, b: 5, c: 8 },
|
69
|
+
{ a: 1, b: 5, c: 9 },
|
70
|
+
{ a: 1, b: 6, c: 7 },
|
71
|
+
{ a: 1, b: 6, c: 8 },
|
72
|
+
{ a: 1, b: 6, c: 9 },
|
73
|
+
|
74
|
+
{ a: 2, b: 4, c: 7 },
|
75
|
+
{ a: 2, b: 4, c: 8 },
|
76
|
+
{ a: 2, b: 4, c: 9 },
|
77
|
+
{ a: 2, b: 5, c: 7 },
|
78
|
+
{ a: 2, b: 5, c: 8 },
|
79
|
+
{ a: 2, b: 5, c: 9 },
|
80
|
+
{ a: 2, b: 6, c: 7 },
|
81
|
+
{ a: 2, b: 6, c: 8 },
|
82
|
+
{ a: 2, b: 6, c: 9 },
|
83
|
+
|
84
|
+
{ a: 3, b: 4, c: 7 },
|
85
|
+
{ a: 3, b: 4, c: 8 },
|
86
|
+
{ a: 3, b: 4, c: 9 },
|
87
|
+
{ a: 3, b: 5, c: 7 },
|
88
|
+
{ a: 3, b: 5, c: 8 },
|
89
|
+
{ a: 3, b: 5, c: 9 },
|
90
|
+
{ a: 3, b: 6, c: 7 },
|
91
|
+
{ a: 3, b: 6, c: 8 },
|
92
|
+
{ a: 3, b: 6, c: 9 },
|
93
|
+
]
|
94
|
+
````
|
95
|
+
|
96
|
+
Notes:
|
97
|
+
|
98
|
+
* Matrix implements Ruby's Enumerable, which means you have the ability to iterate over each hash combination like you would any other Enumerable object.
|
99
|
+
* Values can be arrays and it still works. For example, `#add` (singular form) will honor the array data type: `matrix.add(:a, [1,2,3])` is **not** the same as: `matrix.add_each(:a, [1,2,3])` but it **is** the same as: `matrix.add(:a, [[1,2,3]])`
|
100
|
+
* Keys are type-sensitive and work just like Hash keys work.
|
101
|
+
|
102
|
+
### Record: The Hash Prototype
|
103
|
+
|
104
|
+
HashMath::Record generates a prototype hash and ensures all derived hashes conform to its shape.
|
105
|
+
|
106
|
+
Say we have a person hash, for example:
|
107
|
+
|
108
|
+
````ruby
|
109
|
+
{
|
110
|
+
id: 1,
|
111
|
+
name: 'Matt',
|
112
|
+
location: 'Chicago'
|
113
|
+
}
|
114
|
+
````
|
115
|
+
|
116
|
+
we could create a record modeling this:
|
117
|
+
|
118
|
+
````ruby
|
119
|
+
record = HashMath::Record.new(%i[id name location], 'UNKNOWN')
|
120
|
+
````
|
121
|
+
|
122
|
+
Then, we could shape all other hashes using it to ensure each key is populated. If a key is not populated, the base value will be used ('UNKNOWN' in our case.) For example:
|
123
|
+
|
124
|
+
````ruby
|
125
|
+
records = [
|
126
|
+
record.make(id: 1, name: 'Matt', location: 'Chicago', dob: nil),
|
127
|
+
record.make(id: 2, age: 24),
|
128
|
+
record.make(id: 3, location: 'Los Angeles')
|
129
|
+
]
|
130
|
+
````
|
131
|
+
|
132
|
+
Note: The keys `dob` and `age` appeared in the input but will be ignored as they do not conform to the Record. You could make this strict and raise an error by calling `#make!` instead of `#make`.
|
133
|
+
|
134
|
+
our `records` would now equate to:
|
135
|
+
|
136
|
+
````ruby
|
137
|
+
[
|
138
|
+
{ id: 1, name: 'Matt', location: 'Chicago' },
|
139
|
+
{ id: 2, name: 'UNKNOWN', location: 'UNKNOWN' },
|
140
|
+
{ id: 3, name: 'UNKNOWN', location: 'Los Angeles' },
|
141
|
+
]
|
142
|
+
````
|
143
|
+
|
144
|
+
Notes:
|
145
|
+
|
146
|
+
* keys are type-sensitive and works just like Hash keys work.
|
147
|
+
|
148
|
+
### Table: The Double Hash (Hash of Hashes)
|
149
|
+
|
150
|
+
HashMath::Table builds on top of HashMath::Record. It constructs a table data-structure using a key-value pair builder method: `#add(row_id, field_id, value)`. It ultimately outputs an array of Row objects where each Row object has a row_id and fields (field_id => value) hash. The value proposition here is you can iterate over a key-value pair and construct each row any way you wish.
|
151
|
+
|
152
|
+
Building on our Record example above:
|
153
|
+
|
154
|
+
````ruby
|
155
|
+
record = HashMath::Record.new(%i[name location], 'UNKNOWN')
|
156
|
+
|
157
|
+
table = HashMath::Table.new(record)
|
158
|
+
.add(1, :name, 'Matt')
|
159
|
+
.add(1, :location, 'Chicago')
|
160
|
+
.add(2, :name, 'Nick')
|
161
|
+
.add(3, :location, 'Los Angeles')
|
162
|
+
.add(2, :name 'Nicholas') # notice the secondary call to "2, :name"
|
163
|
+
|
164
|
+
rows = table.to_a
|
165
|
+
````
|
166
|
+
|
167
|
+
which would set our variable `rows` to:
|
168
|
+
|
169
|
+
````ruby
|
170
|
+
[
|
171
|
+
HashMath::Row.new(1, { name: 'Matt', location: 'Chicago' }),
|
172
|
+
HashMath::Row.new(2, { name: 'Nicholas', location: 'UNKNOWN' }),
|
173
|
+
HashMath::Row.new(3, { name: 'UNKNOWN', location: 'Los Angeles' })
|
174
|
+
]
|
175
|
+
````
|
176
|
+
|
177
|
+
Notes:
|
178
|
+
|
179
|
+
* `#add` will throw a KeyOutOfBoundsError if the key is not found.
|
180
|
+
* key is type-sensitive and works just like Hash keys work.
|
181
|
+
|
182
|
+
### Unpivot: Hash Key Coalescence and Row Extrapolation
|
183
|
+
|
184
|
+
Sometimes the expected interface is column-based, but the actual data store is row-based. This causes an impedance between the input set and the persistable data set. HashMath::Unpivot has the ability to extrapolate one hash (row) into multiple hashes (rows) while unpivoting specific keys into key-value pairs.
|
185
|
+
|
186
|
+
For example: say we have a database table persisting key-value pairs of patient attributes:
|
187
|
+
|
188
|
+
patient_id | field | value
|
189
|
+
---------- | --------------- | ----------
|
190
|
+
2 | first_exam_date | 2020-01-03
|
191
|
+
2 | last_exam_date | 2020-04-05
|
192
|
+
2 | consent_date | 2020-01-02
|
193
|
+
|
194
|
+
But our input data looks like this:
|
195
|
+
|
196
|
+
````ruby
|
197
|
+
patient = {
|
198
|
+
patient_id: 2,
|
199
|
+
first_exam_date: '2020-01-03',
|
200
|
+
last_exam_date: '2020-04-05',
|
201
|
+
consent_date: '2020-01-02'
|
202
|
+
}
|
203
|
+
````
|
204
|
+
|
205
|
+
We could use a HashMath::Unpivot to go from one hash to three hashes:
|
206
|
+
|
207
|
+
````ruby
|
208
|
+
pivot_set = {
|
209
|
+
pivots: [
|
210
|
+
{
|
211
|
+
keys: %i[first_exam_date last_exam_date consent_date],
|
212
|
+
coalesce_key: :field,
|
213
|
+
coalesce_key_value: :value
|
214
|
+
}
|
215
|
+
]
|
216
|
+
}
|
217
|
+
|
218
|
+
rows = HashMath::Unpivot.new(pivot_set).perform(patient)
|
219
|
+
````
|
220
|
+
|
221
|
+
The `rows` variable should now be equivalent to:
|
222
|
+
|
223
|
+
````ruby
|
224
|
+
[
|
225
|
+
{ patient_id: 2, field: :first_exam_date, value: '2020-01-03' },
|
226
|
+
{ patient_id: 2, field: :last_exam_date, value: '2020-04-05' },
|
227
|
+
{ patient_id: 2, field: :consent_date, value: '2020-01-02' }
|
228
|
+
]
|
229
|
+
````
|
230
|
+
|
231
|
+
Note: `HashMath::Unpivot#add` also exists to update an already instantiated object with new pivot_sets.
|
232
|
+
|
233
|
+
### Mapper: Constant-Time Cross-Mapper
|
234
|
+
|
235
|
+
The general use-case for `HashMath::Mapper` is to store pre-materialized lookups and then use those lookups efficiently to fill in missing data for a hash.
|
236
|
+
|
237
|
+
For example: say we have incoming data representing patients, but this incoming data is value-oriented, not entity/identity-oriented, which may be what we need to ingest it properly:
|
238
|
+
|
239
|
+
````ruby
|
240
|
+
patient = { patient_id: 2, patient_status: 'active', marital_status: 'married' }
|
241
|
+
````
|
242
|
+
|
243
|
+
It is not sufficient for us to use patient_status and marital_status, what we need is the entity identifiers for those values. What we can do is load up a HashMath::Mapper instance with these values and let it do the work for us:
|
244
|
+
|
245
|
+
````ruby
|
246
|
+
patient_statuses = [
|
247
|
+
{ id: 1, name: 'active' },
|
248
|
+
{ id: 2, name: 'inactive' },
|
249
|
+
{ id: 3, name: 'archived' }
|
250
|
+
]
|
251
|
+
|
252
|
+
marital_statuses = [
|
253
|
+
{ id: 1, code: 'single' },
|
254
|
+
{ id: 2, code: 'married' },
|
255
|
+
{ id: 3, code: 'divorced' }
|
256
|
+
]
|
257
|
+
|
258
|
+
mappings = [
|
259
|
+
{
|
260
|
+
lookup: {
|
261
|
+
name: :patient_statuses,
|
262
|
+
by: :name
|
263
|
+
},
|
264
|
+
set: :patient_status_id,
|
265
|
+
value: :patient_status,
|
266
|
+
with: :id
|
267
|
+
},
|
268
|
+
{
|
269
|
+
lookup: {
|
270
|
+
name: :marital_statuses,
|
271
|
+
by: :code
|
272
|
+
},
|
273
|
+
set: :marital_status_id,
|
274
|
+
value: :marital_status,
|
275
|
+
with: :id
|
276
|
+
}
|
277
|
+
]
|
278
|
+
|
279
|
+
mapper = HashMath::Mapper
|
280
|
+
.new(mappings)
|
281
|
+
.add_each(:patient_statuses, patient_statuses)
|
282
|
+
.add_each(:marital_statuses, marital_statuses)
|
283
|
+
|
284
|
+
mapped_patient = mapper.map(patient)
|
285
|
+
````
|
286
|
+
|
287
|
+
The variable `mapped_patient` should now be equal to:
|
288
|
+
|
289
|
+
````ruby
|
290
|
+
{
|
291
|
+
patient_id: 2,
|
292
|
+
patient_status: 'active',
|
293
|
+
marital_status: 'single',
|
294
|
+
patient_status_id: 1,
|
295
|
+
marital_status_id: 2
|
296
|
+
}
|
297
|
+
````
|
298
|
+
|
299
|
+
## Contributing
|
300
|
+
|
301
|
+
### Development Environment Configuration
|
302
|
+
|
303
|
+
Basic steps to take to get this repository compiling:
|
304
|
+
|
305
|
+
1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check hash_math.gemspec for versions supported)
|
306
|
+
2. Install bundler (gem install bundler)
|
307
|
+
3. Clone the repository (git clone git@github.com:bluemarblepayroll/hash_math.git)
|
308
|
+
4. Navigate to the root folder (cd hash_math)
|
309
|
+
5. Install dependencies (bundle)
|
310
|
+
|
311
|
+
### Running Tests
|
312
|
+
|
313
|
+
To execute the test suite and code-coverage tool, run:
|
314
|
+
|
315
|
+
````bash
|
316
|
+
bundle exec rspec spec --format documentation
|
317
|
+
````
|
318
|
+
|
319
|
+
Alternatively, you can have Guard watch for changes:
|
320
|
+
|
321
|
+
````bash
|
322
|
+
bundle exec guard
|
323
|
+
````
|
324
|
+
|
325
|
+
Also, do not forget to run Rubocop:
|
326
|
+
|
327
|
+
````bash
|
328
|
+
bundle exec rubocop
|
329
|
+
````
|
330
|
+
|
331
|
+
or run all three in one command:
|
332
|
+
|
333
|
+
````bash
|
334
|
+
bundle exec rake
|
335
|
+
````
|
336
|
+
|
337
|
+
### Publishing
|
338
|
+
|
339
|
+
Note: ensure you have proper authorization before trying to publish new versions.
|
340
|
+
|
341
|
+
After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
|
342
|
+
|
343
|
+
1. Merge Pull Request into master
|
344
|
+
2. Update `lib/hash_math/version.rb` using [semantic versioning](https://semver.org/)
|
345
|
+
3. Install dependencies: `bundle`
|
346
|
+
4. Update `CHANGELOG.md` with release notes
|
347
|
+
5. Commit & push master to remote and ensure CI builds master successfully
|
348
|
+
6. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
349
|
+
|
350
|
+
## Code of Conduct
|
351
|
+
|
352
|
+
Everyone interacting in this codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/bluemarblepayroll/hash_math/blob/master/CODE_OF_CONDUCT.md).
|
353
|
+
|
354
|
+
## License
|
355
|
+
|
356
|
+
This project is MIT Licensed.
|
data/hash_math.gemspec
CHANGED
@@ -19,13 +19,16 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.homepage = 'https://github.com/bluemarblepayroll/hash_math'
|
20
20
|
s.license = 'MIT'
|
21
21
|
|
22
|
-
s.required_ruby_version = '>= 2.
|
22
|
+
s.required_ruby_version = '>= 2.5'
|
23
|
+
|
24
|
+
s.add_dependency('acts_as_hashable', '~>1')
|
23
25
|
|
24
26
|
s.add_development_dependency('guard-rspec', '~>4.7')
|
25
27
|
s.add_development_dependency('pry', '~>0')
|
26
|
-
s.add_development_dependency('
|
28
|
+
s.add_development_dependency('pry-byebug', '~>3')
|
29
|
+
s.add_development_dependency('rake', '~>13.0')
|
27
30
|
s.add_development_dependency('rspec')
|
28
|
-
s.add_development_dependency('rubocop', '~>0.
|
29
|
-
s.add_development_dependency('simplecov', '~>0.
|
30
|
-
s.add_development_dependency('simplecov-console', '~>0.
|
31
|
+
s.add_development_dependency('rubocop', '~>0.88.0')
|
32
|
+
s.add_development_dependency('simplecov', '~>0.18.5')
|
33
|
+
s.add_development_dependency('simplecov-console', '~>0.7.0')
|
31
34
|
end
|