bblib 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +147 -40
- data/lib/array/bbarray.rb +26 -0
- data/lib/bblib/version.rb +1 -1
- data/lib/file/bbfile.rb +28 -2
- data/lib/hash/bbhash.rb +10 -3
- data/lib/hash/hash_path.rb +60 -17
- data/lib/hash/hash_path_proc.rb +107 -14
- data/lib/string/bbstring.rb +10 -12
- data/lib/string/cases.rb +96 -0
- data/lib/string/fuzzy_matcher.rb +18 -15
- data/lib/string/matching.rb +2 -2
- data/lib/time/bbtime.rb +19 -3
- data/lib/time/cron.rb +171 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6ed599d59ab2bd4d0a8cdae4cd61383e23d4e64
|
4
|
+
data.tar.gz: 2a528e9bcdb00c34514420da98cb1f990996f9d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e0db224a5d5c9c8b430066ab093af08e510958b7a6aaa0f530f9a1d9e8e63f2bd9768586bf96643b2935d4f5ca68022c57f9db69067181400557cc6177e4775f
|
7
|
+
data.tar.gz: eab50c5ad0d2cf24e7f83b07d86a969a0c6a8fbe660b3db1c57098c00dc6e5fac480f824c7546fcae5b7eef5377623402d86dc8e366d546d2872ca48d4297e0c
|
data/README.md
CHANGED
@@ -1,10 +1,41 @@
|
|
1
1
|
# BBLib
|
2
2
|
|
3
|
-
BBLib (Brandon-Black-Lib) is a collection of various reusable methods and classes to extend the Ruby language.
|
3
|
+
BBLib (Brandon-Black-Lib) is a collection of various reusable methods and classes to extend the Ruby language.
|
4
4
|
|
5
|
-
One of
|
5
|
+
One of the primary goals with the BBLib is to keep it as lightweight as possible. This means you will not find dependencies outside of the Ruby core libraries.
|
6
6
|
|
7
|
-
For a full breakdown of what is currently in this library, scroll down.
|
7
|
+
For a full breakdown of what is currently in this library, scroll down. For a quick overview of key features, read the following list.
|
8
|
+
|
9
|
+
* __BBLib HashPath:__ Hash path is an XPath or JSONPath like navigation library for native Ruby hashes. It uses dot ('.') delimited path strings to navigate hash AND array objects. What makes hash path stand out is that it can navigate recursively within hashes, arrays, nested hashes, nested arrays, nested hashes within nested arrays within nested arrays with...well, you get the picture. Not only does it support navigation of hashes, it also comes with many functions to easily manipulate hashes by moving paths, copying paths, deleting paths or processing paths (see below for a few examples).
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
myhash = {a:1, b:2, c:{d:[3,4,{e:5},6]}, f:7}
|
13
|
+
p myhash.hash_path('c.d..e')
|
14
|
+
#=> [5]
|
15
|
+
p myhash.hash_path('..d')
|
16
|
+
#=> [3, 4, {:e=>5}, 6]
|
17
|
+
p myhash.hash_path('c.d[1]')
|
18
|
+
#=> [4]
|
19
|
+
p myhash.hash_path('c.d[0..1]')
|
20
|
+
#=> [3, 4]
|
21
|
+
|
22
|
+
# Move key/values
|
23
|
+
p myhash.hash_path_move('a' => 'c.g.h')
|
24
|
+
#=> {:b=>2, :c=>{:d=>[3, 4, {:e=>5}, 6], :g=>{:h=>1}}, :f=>7}
|
25
|
+
|
26
|
+
# Copy key/values
|
27
|
+
p myhash.hash_path_copy('b' => 'z')
|
28
|
+
#=> {:a=>1, :b=>2, :c=>{:d=>[3, 4, {:e=>5}, 6]}, :f=>7, :z=>2}
|
29
|
+
```
|
30
|
+
* __Deep Merge:__ A deep merge algorithm is included that can merge hashes with nested hashes or nested arrays or nested hashes with nested arrays with nested hashes and so on... It can also combine colliding values into arrays rather than overwriting using a toggle-able overwrite flag.
|
31
|
+
* __File & Time Parsing From Strings:__ Have a string such as '1MB 15KB' and want to make it numeric? Look no further. BBLib has methods to parse files size expressions and duration expressions from strings (like '1min 10sec'). Nearly any variant of size or duration expression is supported. For instance, '1sec', '1s', '1 s', '1 second', '1secs' are all properly parsed as 1 second.
|
32
|
+
* __Fuzzy String Matching:__ The BBLib has implementations of a few string comparison algorithms. Most noteworthy, it features a simple implementation of the Levenshtein distance algorithm. A class, FuzzyMatcher, is also included to perform weight comparisons of strings using any of the included algorithms.
|
33
|
+
* __Convert Roman Numerals__ within strings to Integers and Integers to Roman Numerals.
|
34
|
+
* __Normalize articles__ such as 'the', 'an' and 'a' within titles to be displayed in the front, back or be stripped entirely. Helpful for sorting titles or for string comparisons.
|
35
|
+
* __Object to Hash:__ Turn any object and its instance variables as well as nested objects and their instance variables into a hash. Handy to have alongside hash path.
|
36
|
+
* __TaskTimer:__ A simple and easy to use timer class to compliment benchmarking in code by timing various tasks or groups of tasks. History is kept so that averages, sums, mins and maxes can be checked per task.
|
37
|
+
* __Recursive File Scanners:__ A few file and directory scanners are implemented that recursively (by toggle) scan directories looking for files matching given filters.
|
38
|
+
* __Plus more...__
|
8
39
|
|
9
40
|
## Installation
|
10
41
|
|
@@ -24,16 +55,6 @@ Or install it yourself as:
|
|
24
55
|
|
25
56
|
## Usage
|
26
57
|
|
27
|
-
BBLib is currently broken up into the following categories:
|
28
|
-
* File
|
29
|
-
* Hash
|
30
|
-
* Math
|
31
|
-
* Net
|
32
|
-
* String
|
33
|
-
* Time
|
34
|
-
|
35
|
-
|
36
|
-
|
37
58
|
### File
|
38
59
|
#### File Scanners
|
39
60
|
|
@@ -75,7 +96,7 @@ BBLib.scan_dir 'C:/path/to/files', recursive: true, filter: ['*.jpg', '*.txt']
|
|
75
96
|
#=> 'C:/path/to/files/folder/another_folder/text.txt'
|
76
97
|
```
|
77
98
|
|
78
|
-
In addition, both _scan_files_ and _scan_dirs_ also support a **mode** named argument. By default, this argument is set to :path. In _scan_files_ if :file is passed to :mode, a ruby File object will be returned rather than a String representation of the path.
|
99
|
+
In addition, both _scan_files_ and _scan_dirs_ also support a **mode** named argument. By default, this argument is set to :path. In _scan_files_ if :file is passed to :mode, a ruby File object will be returned rather than a String representation of the path. Similarly, if :dir is passed to _scan_dirs_ a ruby Dir object is returned, rather than just a string.
|
79
100
|
|
80
101
|
#### File Size Parsing
|
81
102
|
|
@@ -159,11 +180,11 @@ h1.deep_merge h2, merge_arrays: false
|
|
159
180
|
#=> {:value=>5, :array=>[6, 7], :hash=>{:a=>1, :b_hash=>{:c=>9, :d=>10, :y=>10}, :z=>nil}}
|
160
181
|
```
|
161
182
|
|
162
|
-
A
|
183
|
+
A ! version of _deep_merge_ is also available to modify the hash in place rather than returning a new hash.
|
163
184
|
|
164
185
|
#### Keys To Sym
|
165
186
|
|
166
|
-
Convert all keys within a hash (including nested keys) to symbols. This is useful after parsing json if you prefer to work with symbols rather than strings. An
|
187
|
+
Convert all keys within a hash (including nested keys) to symbols. This is useful after parsing json if you prefer to work with symbols rather than strings. An in-place (**!**) version of the method is also available.
|
167
188
|
|
168
189
|
```ruby
|
169
190
|
h = {"author" => "Tom Clancy", "books" => ["Rainbow Six", "The Hunt for Red October"]}
|
@@ -171,6 +192,12 @@ h.keys_to_sym
|
|
171
192
|
#=> {:author=>"Tom Clancy", :books=>["Rainbow Six", "The Hunt for Red October"]}
|
172
193
|
```
|
173
194
|
|
195
|
+
_Note: This is similar to what Rails provides, except it even converts keys within nested hashes or nested arrays that contain nested hashes._
|
196
|
+
|
197
|
+
#### Keys To Str
|
198
|
+
|
199
|
+
The same as keys to sym, but it converts keys to strings rather than symbols.
|
200
|
+
|
174
201
|
#### Reverse
|
175
202
|
|
176
203
|
Similar to reverse for Array. Calling this will reverse the current order of the Hash's keys. An in place version is also available.
|
@@ -207,11 +234,6 @@ BBLib.keep_betwee number, nil, 100
|
|
207
234
|
|
208
235
|
|
209
236
|
|
210
|
-
### Net
|
211
|
-
Currently empty...
|
212
|
-
|
213
|
-
|
214
|
-
|
215
237
|
### String
|
216
238
|
|
217
239
|
#### FuzzyMatcher
|
@@ -301,10 +323,29 @@ Checks to see how many words in a string match another. Words must match exactly
|
|
301
323
|
#=> 66.66666666666666
|
302
324
|
```
|
303
325
|
|
304
|
-
4 - Numeric Similarity (In Progress)
|
326
|
+
4 - Numeric Similarity _(In Progress)_
|
305
327
|
|
306
328
|
This algorithm is currently undergoing refactoring...
|
307
329
|
|
330
|
+
This is primarily for comparing titles (such as movie or game titles). As an example, other algorithms would conclude that _'Terminator 2'_ is more similar to _'Terminator'_ than _'Terminator 2: Judgement Day'_, but the best match may really be _'Terminator 2: Judgement Day'_. To fix this, the numeric similarity would weight more towards the more appropriate title that contains the same number or numbers as itself. A string with no numbers is effectively considered to include a 1 for comparison's sake.
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
a = 'Terminator 2'
|
334
|
+
b = 'Terminator 2: Judgement Day'
|
335
|
+
c = 'Terminator'
|
336
|
+
|
337
|
+
puts a.levenshtein_similarity c
|
338
|
+
#=> 83.33333333333334
|
339
|
+
puts a.numeric_similarity c
|
340
|
+
#=> 33.33333333333333
|
341
|
+
|
342
|
+
puts a.levenshtein_similarity b
|
343
|
+
#=> 44.44444444444444
|
344
|
+
puts a.numeric_similarity b
|
345
|
+
#=> 100.0
|
346
|
+
```
|
347
|
+
This algorithm is generally only useful when combined with another algorithm, which is exactly what the FuzzyMatcher class does.
|
348
|
+
|
308
349
|
5 - QWERTY Similarity
|
309
350
|
|
310
351
|
A basic method that compares two strings by measuring the physical difference from one char to another on a QWERTY keyboard (alpha-numeric only). May be useful for detecting typos in words, but becomes less useful depending on the length of the string. This method is still in development and not yet in a final state. Currently a total distance is returned. Eventually, a percentage based match will replace this.
|
@@ -371,15 +412,15 @@ BBLib.from_roman "Toy Story III"
|
|
371
412
|
|
372
413
|
**msplit** _aka multi split_
|
373
414
|
|
374
|
-
_msplit_ is similar to the String method split, except it can take an array of string delimiters rather than a single
|
415
|
+
_msplit_ is similar to the String method split, except it can take an array of string delimiters rather than a single delimiter. The string is split be each delimiter in order and an Array is returned. msplit may also be called on an array to split elements within it.
|
375
416
|
|
376
417
|
```ruby
|
377
|
-
"This_is.a&&&&test".msplit
|
418
|
+
"This_is.a&&&&test".msplit '_', '.', '&'
|
378
419
|
|
379
420
|
#=> ['This', 'is', 'a', 'test']
|
380
421
|
```
|
381
422
|
|
382
|
-
By default any empty items from the
|
423
|
+
By default any empty items from the returned Array are removed. This behavior can be changed using the _:keep_empty_ named param.
|
383
424
|
|
384
425
|
```ruby
|
385
426
|
"This_is.a&&&&test".msplit ['_', '.', '&'], keep_empty: true
|
@@ -387,8 +428,6 @@ By default any empty items from the return Array are removed. This behavior can
|
|
387
428
|
#=> ['This', 'is', 'a', '', '', '', 'test']
|
388
429
|
```
|
389
430
|
|
390
|
-
_msplit is only available directly from an instantiated String object._
|
391
|
-
|
392
431
|
**move_articles**
|
393
432
|
|
394
433
|
This method is used to normalize strings that contain titles. It parses a string and checks to see if _the_, _an_ or _a_ are in the title, either preceding or trailing. If they are found they are moved to the front, back or removed depending on the argument passed to _position_.
|
@@ -398,38 +437,85 @@ The method is available via the BBLib module or any instance of String.
|
|
398
437
|
```ruby
|
399
438
|
title = "The Simpsons"
|
400
439
|
title.move_articles :back
|
401
|
-
|
402
440
|
#=> "Simpons, The"
|
403
441
|
|
404
442
|
title.move_articles :none
|
405
|
-
|
406
443
|
#=> "Simpsons"
|
407
444
|
|
408
445
|
title = "Day to Remember, A"
|
409
446
|
title.move_articles :front
|
410
|
-
|
411
447
|
#=> "A Day to Remember"
|
412
448
|
```
|
413
449
|
|
414
|
-
**
|
450
|
+
**extract_integers**/**extract_floats**/**extract_numbers**
|
451
|
+
|
452
|
+
Three methods to grab numbers from within strings. Integers only nabs numbers with no decimal places, floats gets only numbers with a decimal and numbers gets both integers and floats. The numbers must also be properly formatted, so something like the version number '2.1.1' below will not be extracted.
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
s = 'Test 10 2.5 Number 100 aaaa 10.113 Version 2.1.1'
|
456
|
+
|
457
|
+
p s.extract_integers
|
458
|
+
#=> [10, 100]
|
459
|
+
p s.extract_floats
|
460
|
+
#=> [2.5, 10.113]
|
461
|
+
p s.extract_numbers
|
462
|
+
#=> [10, 2.5, 100, 10.113]
|
463
|
+
```
|
464
|
+
|
465
|
+
### Time
|
466
|
+
|
467
|
+
#### Cron
|
468
|
+
|
469
|
+
BBLib includes a lightweight cron syntax parser. It can be used to display the runtimes of a cron based on a cron string. Nearly every variant of cron syntax is supported with the ability to intermix ranges, divisors and explicit numbers in the same interval placing.
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
|
473
|
+
cron = BBLib::Cron.new('* * * * * *')
|
474
|
+
puts cron.next
|
475
|
+
#=> 2016-04-03 22:01:00 -0600
|
415
476
|
|
416
|
-
|
477
|
+
puts cron.previous
|
478
|
+
#=> 2016-04-03 21:59:00 -0600
|
417
479
|
|
418
|
-
|
480
|
+
p cron.next(5)
|
481
|
+
#=> [2016-04-03 22:01:00 -0600, 2016-04-03 22:02:00 -0600, 2016-04-03 22:03:00 -0600, 2016-04-03 22:04:00 -0600, 2016-04-03 22:05:00 -0600]
|
419
482
|
|
420
|
-
|
483
|
+
# Set the time explicitly. The default is the current system time.
|
484
|
+
puts cron.next(time: Time.now+30)
|
485
|
+
#=> 2016-04-03 22:31:00 -0600
|
486
|
+
```
|
421
487
|
|
422
|
-
|
488
|
+
An instantiated Cron object is not necessary to get the next and previous times.
|
423
489
|
|
424
|
-
|
490
|
+
```ruby
|
491
|
+
puts BBLib::Cron.next('* * * * * *')
|
492
|
+
#=> 2016-04-03 22:04:00 -0600
|
425
493
|
|
426
|
-
|
494
|
+
puts BBLib::Cron.next('0-5 * * * * *')
|
495
|
+
#=> 2016-04-03 22:04:00 -0600
|
427
496
|
|
428
|
-
|
497
|
+
puts BBLib::Cron.next('0 1 1 1 1 *')
|
498
|
+
#=> 2018-01-01 01:00:00 -0700
|
429
499
|
|
500
|
+
puts BBLib::Cron.next('1 1 1-5 * * 2020')
|
501
|
+
#=> 2020-01-01 01:01:00 -0700
|
430
502
|
|
503
|
+
puts BBLib::Cron.next('*/5 * * * * *')
|
504
|
+
#=> 2016-04-03 22:05:00 -0600
|
431
505
|
|
432
|
-
|
506
|
+
puts BBLib::Cron.next('1-3,4,5,10-11 1-10 */5 * * *')
|
507
|
+
#=> 2016-04-06 01:01:00 -0600
|
508
|
+
```
|
509
|
+
|
510
|
+
Common vixieisms are also supported:
|
511
|
+
|
512
|
+
```ruby
|
513
|
+
puts BBLib::Cron.next('@daily')
|
514
|
+
#=> 2016-04-04 00:00:00 -0600
|
515
|
+
|
516
|
+
puts BBLib::Cron.next('@weekly')
|
517
|
+
#=> 2016-04-10 00:00:00 -0600
|
518
|
+
```
|
433
519
|
|
434
520
|
#### Duration parser
|
435
521
|
|
@@ -447,7 +533,14 @@ Similar to the file size parser under the files section, but instead can parse d
|
|
447
533
|
#=> 1.1697222222222223
|
448
534
|
```
|
449
535
|
Output options are:
|
450
|
-
* :
|
536
|
+
* :yocto
|
537
|
+
* :zepto
|
538
|
+
* :atto
|
539
|
+
* :femto
|
540
|
+
* :pico
|
541
|
+
* :nano
|
542
|
+
* :micro
|
543
|
+
* :milli
|
451
544
|
* :sec
|
452
545
|
* :min
|
453
546
|
* :hour
|
@@ -456,6 +549,20 @@ Output options are:
|
|
456
549
|
* :month
|
457
550
|
* :year
|
458
551
|
|
552
|
+
__WARNING:__ _time intervals below microseconds are prone to heavy rounding errors in the current implementation. They are NOT EXACT._
|
553
|
+
|
554
|
+
The colon separated duration pattern (eg. '02:30') can also be matched. The last set of digits is treated as seconds with each prior number being one interval greater. The default starting interval can be changed using the __min_interval__ named param. The available options are the same as the output options. This pattern type can even be intermixed with the types shown above and will be added to the total duration.
|
555
|
+
|
556
|
+
```ruby
|
557
|
+
duration = '04:35'
|
558
|
+
|
559
|
+
puts duration.parse_duration
|
560
|
+
#=> 275.0
|
561
|
+
|
562
|
+
puts duration.parse_duration min_interval: :min
|
563
|
+
#=> 16500.0
|
564
|
+
```
|
565
|
+
|
459
566
|
**Create a duration String from Numeric**
|
460
567
|
|
461
568
|
There is also a method to turn a Numeric object into a string representation of a duration. This method is extended to the Numeric class. An input may be specified to tell the method what the input number represents. The options for this are the same as the output options listed above. A stop can be added using any of those same options. This will prevent the string from containing anything below the specified time type. For instance, specifying _stop: :sec_ will prevent milliseconds from being included if there are any. There are also three options that can be passed to the _:style_ argument to change the output (options are _:full_, _:medium_ and _:short:).
|
data/lib/array/bbarray.rb
CHANGED
@@ -1,4 +1,18 @@
|
|
1
1
|
|
2
|
+
module BBLib
|
3
|
+
|
4
|
+
# Takes two arrays (can be of different length) and interleaves them like [a[0], b[0], a[1], b[1]...]
|
5
|
+
def self.interleave a, b, filler: nil
|
6
|
+
if a.size < b.size
|
7
|
+
a = a.dup
|
8
|
+
while a.size < b.size
|
9
|
+
a.push filler
|
10
|
+
end
|
11
|
+
end
|
12
|
+
a.zip(b).flatten(1)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
2
16
|
|
3
17
|
class Array
|
4
18
|
def msplit *delims, keep_empty: false
|
@@ -12,4 +26,16 @@ class Array
|
|
12
26
|
def keys_to_s clean: false
|
13
27
|
self.map{ |v| Hash === v || Array === v ? v.keys_to_s : v }
|
14
28
|
end
|
29
|
+
|
30
|
+
def to_xml level: 0, key:nil
|
31
|
+
map do |v|
|
32
|
+
nested = v.respond_to?(:to_xml)
|
33
|
+
value = nested ? v.to_xml(level:level+1, key:key) : v
|
34
|
+
"\t"*level + "<#{key}>\n" + (nested ? '' : "\t"*(level+1)) + "#{value}\n" + "\t"*level + "</#{key}>\n"
|
35
|
+
end.join
|
36
|
+
end
|
37
|
+
|
38
|
+
def interleave b, filler: nil
|
39
|
+
BBLib.interleave self, b, filler: filler
|
40
|
+
end
|
15
41
|
end
|
data/lib/bblib/version.rb
CHANGED
data/lib/file/bbfile.rb
CHANGED
@@ -7,7 +7,7 @@ module BBLib
|
|
7
7
|
if !filter.nil?
|
8
8
|
filter = [filter].flatten.map{ |f| path.to_s + (recursive ? '/**/' : '/') + f.to_s }
|
9
9
|
else
|
10
|
-
filter = path.to_s + (recursive ? '/**/*' : '/*')
|
10
|
+
filter = (path.to_s + (recursive ? '/**/*' : '/*')).gsub('//', '/')
|
11
11
|
end
|
12
12
|
Dir.glob(filter)
|
13
13
|
end
|
@@ -44,7 +44,29 @@ module BBLib
|
|
44
44
|
return bytes / FILE_SIZES[output][:mult]
|
45
45
|
end
|
46
46
|
|
47
|
-
|
47
|
+
# A mostly platform agnostic call to get root volumes
|
48
|
+
def self.root_dirs
|
49
|
+
begin # For windows
|
50
|
+
`wmic logicaldisk get name`.split("\n").map{ |m| m.strip }[1..-1].reject{ |r| r == '' }
|
51
|
+
rescue
|
52
|
+
begin # Windows attempt 2
|
53
|
+
`fsutil fsinfo drives`.scan(/(?<=\s)\w\:/)
|
54
|
+
rescue # Linux
|
55
|
+
begin
|
56
|
+
`ls /`.split("\n").map{ |m| m.strip }.reject{ |r| r == '' }
|
57
|
+
rescue # All attempts failed
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Windows only method to get the volume labels of disk drives
|
65
|
+
def self.root_volume_labels
|
66
|
+
`wmic logicaldisk get caption,volumename`.split("\n")[1..-1].map{ |m| [m.split(" ").first.to_s.strip, m.split(" ")[1..-1].to_a.join(' ').strip] }.reject{ |o,t| o == '' }.to_h
|
67
|
+
end
|
68
|
+
|
69
|
+
FILE_SIZES = {
|
48
70
|
byte: { mult: 1, exp: ['b', 'byt', 'byte'] },
|
49
71
|
kilobyte: { mult: 1024, exp: ['kb', 'kilo', 'k', 'kbyte', 'kilobyte'] },
|
50
72
|
megabyte: { mult: 1048576, exp: ['mb', 'mega', 'm', 'mib', 'mbyte', 'megabyte'] },
|
@@ -73,6 +95,10 @@ class String
|
|
73
95
|
self[(self.include?('/') ? self.rindex('/').to_i+1 : 0)..(with_extension ? -1 : self.rindex('.').to_i-1)]
|
74
96
|
end
|
75
97
|
|
98
|
+
def dirname
|
99
|
+
self.scan(/.*(?=\/)/).first
|
100
|
+
end
|
101
|
+
|
76
102
|
def parse_file_size output: :byte
|
77
103
|
BBLib.parse_file_size(self, output:output)
|
78
104
|
end
|
data/lib/hash/bbhash.rb
CHANGED
@@ -2,12 +2,10 @@ require_relative 'hash_path'
|
|
2
2
|
|
3
3
|
class Hash
|
4
4
|
|
5
|
-
|
6
|
-
|
7
5
|
# Merges with another hash but also merges all nested hashes and arrays/values.
|
8
6
|
# Based on method found @ http://stackoverflow.com/questions/9381553/ruby-merge-nested-hash
|
9
7
|
def deep_merge with, merge_arrays: true, overwrite_vals: true
|
10
|
-
merger = proc{ |k, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : (merge_arrays && v1.is_a?(Array) && v2.is_a?(Array) ? (v1 + v2) : (overwrite_vals ? v2 : [v1, v2].flatten)) }
|
8
|
+
merger = proc{ |k, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : (merge_arrays && v1.is_a?(Array) && v2.is_a?(Array) ? (v1 + v2) : (overwrite_vals || v1 == v2 ? v2 : [v1, v2].flatten)) }
|
11
9
|
self.merge(with, &merger)
|
12
10
|
end
|
13
11
|
|
@@ -50,4 +48,13 @@ class Hash
|
|
50
48
|
replace hash.merge(self).merge(hash)
|
51
49
|
end
|
52
50
|
|
51
|
+
def to_xml level: 0, key:nil
|
52
|
+
map do |k,v|
|
53
|
+
nested = v.respond_to?(:to_xml)
|
54
|
+
array = Array === v
|
55
|
+
value = nested ? v.to_xml(level:level+(array ? 0 : 1), key:k) : v
|
56
|
+
"\t" * level + (array ? '' : "<#{k}>\n") + (nested ? '' : "\t" * (level+1)) + "#{value}\n" + "\t" * level + (array ? '' : "</#{k}>\n")
|
57
|
+
end.join
|
58
|
+
end
|
59
|
+
|
53
60
|
end
|
data/lib/hash/hash_path.rb
CHANGED
@@ -11,13 +11,13 @@ module BBLib
|
|
11
11
|
hashes.each do |hash|
|
12
12
|
if recursive
|
13
13
|
patterns = Regexp === p[:key] ? p[:key] : p[:key].to_s == '*' ? /.*/ : (symbol_sensitive ? p[:key] : [p[:key].to_sym, p[:key].to_s])
|
14
|
-
|
14
|
+
hash.dig(patterns)[p[:slice]].each{ |va| matches.push va }
|
15
15
|
else
|
16
16
|
if p[:key].nil?
|
17
|
-
if hash.is_a?(Array) then
|
17
|
+
if hash.is_a?(Array) then hash[p[:slice]].each{ |h| matches << h } end
|
18
18
|
elsif Symbol === p[:key] || String === p[:key]
|
19
19
|
if p[:key].to_s == '*'
|
20
|
-
|
20
|
+
hash.values[p[:slice]].each{ |va| matches.push va }
|
21
21
|
else
|
22
22
|
next unless symbol_sensitive ? hash.include?(p[:key]) : (hash.include?(p[:key].to_sym) || hash.include?(p[:key].to_s) )
|
23
23
|
mat = (symbol_sensitive ? hash[p[:key]] : ( if hash.include?(p[:key].to_sym) then hash[p[:key].to_sym] else hash[p[:key].to_s] end ))
|
@@ -30,11 +30,9 @@ module BBLib
|
|
30
30
|
end
|
31
31
|
matches = BBLib.analyze_hash_path_formula p[:formula], matches
|
32
32
|
if path.size > 1 && !matches.empty?
|
33
|
-
# p "MAT #{matches}"
|
34
|
-
# matches.map!{ |m| m.is_a?(Array) ? [m] : m }
|
35
33
|
BBLib.hash_path(matches.reject{ |r| !(r.is_a?(Hash) || r.is_a?(Array)) }, path[1..-1], symbol_sensitive:symbol_sensitive)
|
36
34
|
else
|
37
|
-
return matches
|
35
|
+
return matches
|
38
36
|
end
|
39
37
|
end
|
40
38
|
|
@@ -44,10 +42,14 @@ module BBLib
|
|
44
42
|
details[:paths].each do |path, d|
|
45
43
|
d[:hashes].each do |h|
|
46
44
|
count+=1
|
47
|
-
|
45
|
+
if d[:last][:key].is_a?(Regexp)
|
46
|
+
exists = h.keys.any?{ |k| k.to_s =~ d[:last][:key] }
|
47
|
+
else
|
48
|
+
exists = (details[:symbol_sensitive] ? h.include?(d[:last][:key]) : (h.include?(d[:last][:key].to_sym) || h.include?(d[:last][:key].to_s) ))
|
49
|
+
end
|
48
50
|
next unless details[:bridge] || exists
|
49
|
-
key = details[:symbol_sensitive] ? d[:last][:key] : (h.include?(d[:last][:key].to_sym) ? d[:last][:key].to_sym : d[:last][:key].to_s)
|
50
|
-
if details[:symbols] then key = key.to_sym elsif !exists then key = d[:last][:key] end
|
51
|
+
key = details[:symbol_sensitive] || d[:last][:key].is_a?(Regexp) ? d[:last][:key] : (h.include?(d[:last][:key].to_sym) ? d[:last][:key].to_sym : d[:last][:key].to_s)
|
52
|
+
# if details[:symbols] then key = key.to_sym elsif !exists then key = d[:last][:key] end
|
51
53
|
if Fixnum === d[:last][:slice]
|
52
54
|
h[key][d[:last][:slice]] = d[:value]
|
53
55
|
else
|
@@ -158,27 +160,32 @@ module BBLib
|
|
158
160
|
private
|
159
161
|
|
160
162
|
def self.hash_path_analyze path
|
163
|
+
return {key: '', slice: (0..-1), formula: nil} if path == '' || path.nil?
|
161
164
|
key = path.scan(/\A.*^[^\[\(\{]*/i).first.to_s
|
162
|
-
if key.encap_by?('/')
|
165
|
+
if key.encap_by?('/') || key.start_with?('/') && key.end_with?('i')
|
163
166
|
key = eval(key)
|
164
167
|
elsif key.start_with? ':'
|
165
168
|
key = key[1..-1].to_sym
|
166
169
|
end
|
167
170
|
slice = eval(path.scan(/(?<=\[).*?(?=\])/).first.to_s)
|
168
|
-
|
171
|
+
no_slice = false
|
172
|
+
formula = path.scan(/(?<=\().*(?=\))/).first
|
173
|
+
if !slice.is_a?(Range) && !slice.is_a?(Fixnum) then slice = (0..-1); no_slice = true end
|
174
|
+
if (key.nil? || key == '') && (!slice.nil? || !no_slice) then key = nil end
|
169
175
|
if slice.nil? then slice = (0..-1) end
|
170
|
-
formula = path.scan(/(?<=\().*?(?=\))/).first
|
171
|
-
if key.empty? && slice != (0..-1) then key = nil end
|
172
176
|
{key:key, slice:slice, formula:formula}
|
173
177
|
end
|
174
178
|
|
175
179
|
def self.split_hash_path path, delimiter = '.'
|
176
180
|
if path.to_s.start_with?(delimiter) then path = path.to_s.sub(delimiter, '') end
|
177
|
-
paths, stop, open = [], 0, false
|
181
|
+
paths, stop, open, popen, ropen = [], 0, false, false, false
|
178
182
|
path.chars.each do |t|
|
179
183
|
if t == '[' then open = true end
|
180
184
|
if t == ']' then open = false end
|
181
|
-
if t ==
|
185
|
+
if t == '(' then popen = true end
|
186
|
+
if t == ')' then popen = false end
|
187
|
+
if t == '/' then ropen = !ropen end
|
188
|
+
if t == delimiter && !open && !popen && !ropen then paths << path[0..stop].reverse.sub(delimiter,'').reverse; path = path[stop+1..-1]; stop = -1 end
|
182
189
|
stop += 1
|
183
190
|
end
|
184
191
|
paths << path
|
@@ -193,7 +200,7 @@ module BBLib
|
|
193
200
|
temp = []
|
194
201
|
hashes.flatten.each do |p|
|
195
202
|
begin
|
196
|
-
if eval(formula.gsub('$', p.to_s))
|
203
|
+
if eval(p.is_a?(Hash) ? formula.gsub('$', "(#{p})") : formula.gsub('$', p.to_s))
|
197
204
|
temp << p
|
198
205
|
end
|
199
206
|
rescue StandardError, SyntaxError => se
|
@@ -230,7 +237,7 @@ module BBLib
|
|
230
237
|
symbol_sensitive: {default:false},
|
231
238
|
stop_on_nil: {default:true},
|
232
239
|
arrays: {default:[]},
|
233
|
-
keys_to_sym: {default:
|
240
|
+
keys_to_sym: {default:false}
|
234
241
|
}
|
235
242
|
|
236
243
|
end
|
@@ -380,6 +387,42 @@ end
|
|
380
387
|
|
381
388
|
class Array
|
382
389
|
|
390
|
+
def hash_path path, delimiter: '.'
|
391
|
+
BBLib.hash_path self, path, delimiter:delimiter
|
392
|
+
end
|
393
|
+
|
394
|
+
def hash_path_set *args
|
395
|
+
BBLib.hash_path_set self, args
|
396
|
+
end
|
397
|
+
|
398
|
+
def hash_path_copy *args
|
399
|
+
BBLib.hash_path_copy self, args
|
400
|
+
end
|
401
|
+
|
402
|
+
def hash_path_copy_to hash, *args
|
403
|
+
BBLib.hash_path_copy_to self, hash, args
|
404
|
+
end
|
405
|
+
|
406
|
+
def hash_path_move_to hash, *args
|
407
|
+
BBLib.hash_path_move_to self, hash, args
|
408
|
+
end
|
409
|
+
|
410
|
+
def hash_path_move *args
|
411
|
+
BBLib.hash_path_move self, args
|
412
|
+
end
|
413
|
+
|
414
|
+
def hash_path_delete *args
|
415
|
+
BBLib.hash_path_delete self, args
|
416
|
+
end
|
417
|
+
|
418
|
+
def hash_path_keys
|
419
|
+
BBLib.hash_path_keys self
|
420
|
+
end
|
421
|
+
|
422
|
+
def hash_path_exists? path, delimiter: '.', symbol_sensitive: false
|
423
|
+
BBLib.hash_path_exists? self, path, delimiter:delimiter, symbol_sensitive:symbol_sensitive
|
424
|
+
end
|
425
|
+
|
383
426
|
def dig keys
|
384
427
|
matches = []
|
385
428
|
self.each do |i|
|
data/lib/hash/hash_path_proc.rb
CHANGED
@@ -6,21 +6,28 @@ class Hash
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
+
class Array
|
10
|
+
def hash_path_proc action, paths, *args, **params
|
11
|
+
BBLib.hash_path_proc self, action, paths, *args, **params
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
9
15
|
module BBLib
|
10
16
|
|
11
17
|
def self.hash_path_proc hash, action, paths, *args, **params
|
12
18
|
action = HASH_PATH_PROC_TYPES.keys.find{ |k| k == action || HASH_PATH_PROC_TYPES[k][:aliases].include?(action) }
|
13
19
|
return nil unless action
|
14
20
|
paths.to_a.each do |path|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
hash.hash_path(path).each do |value|
|
22
|
+
if params.include?(:condition) && params[:condition]
|
23
|
+
begin
|
24
|
+
next unless eval(params[:condition].gsub('$', value.to_s))
|
25
|
+
rescue StandardError, SyntaxError => e
|
26
|
+
next
|
27
|
+
end
|
21
28
|
end
|
29
|
+
HashPath.send(action, hash, path, value, *args, **params)
|
22
30
|
end
|
23
|
-
HashPath.send(action, hash, path, value, *args, **params)
|
24
31
|
end
|
25
32
|
return hash
|
26
33
|
end
|
@@ -35,22 +42,34 @@ module BBLib
|
|
35
42
|
extract_first: {aliases: [:grab_first, :scan_first]},
|
36
43
|
extract_last: {aliases: [:grab_last, :scan_last]},
|
37
44
|
parse_date: { aliases: [:date, :parse_time, :time]},
|
45
|
+
parse_date_unix: { aliases: [:unix_time, :unix_date]},
|
38
46
|
parse_duration: { aliases: [:duration]},
|
39
47
|
parse_file_size: { aliases: [:file_size]},
|
40
48
|
to_string: {aliases: [:to_s, :stringify]},
|
41
49
|
downcase: { aliases: [:lower, :lowercase, :to_lower]},
|
42
50
|
upcase: { aliases: [:upper, :uppercase, :to_upper]},
|
43
|
-
# titlecase: { aliases: [:title_case]},
|
44
51
|
roman: { aliases: [:convert_roman, :roman_numeral, :parse_roman]},
|
45
52
|
remove_symbols: { aliases: [:chop_symbols, :drop_symbols]},
|
46
53
|
format_articles: { aliases: [:articles]},
|
47
54
|
reverse: { aliases: [:invert]},
|
48
55
|
delete: { aliases: [:del]},
|
49
56
|
remove: { aliases: [:rem]},
|
50
|
-
custom: {aliases: [:send]}
|
57
|
+
custom: {aliases: [:send]},
|
58
|
+
# TODO
|
59
|
+
# titlecase: { aliases: [:title_case]},
|
60
|
+
encapsulate: {aliases: []},
|
61
|
+
uncapsulate: {aliases: []},
|
62
|
+
extract_integers: {aliases: [:extract_ints]},
|
63
|
+
extract_floats: {aliases: []},
|
64
|
+
extract_numbers: {aliases: []},
|
65
|
+
max_number: {aliases: [:max, :maximum, :maximum_number]},
|
66
|
+
min_number: {aliases: [:min, :minimum, :minimum_number]},
|
67
|
+
avg_number: {aliases: [:avg, :average, :average_number]},
|
68
|
+
sum_number: {aliases: [:sum]},
|
69
|
+
strip: {aliases: [:trim]},
|
51
70
|
# rename: { aliases: [:rename_key]},
|
52
|
-
|
53
|
-
|
71
|
+
concat: { aliases: [:join, :concat_with]},
|
72
|
+
reverse_concat: { aliases: [:reverse_join, :reverse_concat_with]}
|
54
73
|
}
|
55
74
|
|
56
75
|
module HashPath
|
@@ -74,13 +93,13 @@ module BBLib
|
|
74
93
|
|
75
94
|
def self.replace hash, path, value, args, params
|
76
95
|
value = value.dup.to_s
|
77
|
-
args.each{ |k,v| value.gsub!(k
|
96
|
+
args.each{ |k,v| value.gsub!(k, v.to_s) }
|
78
97
|
hash.hash_path_set path => value
|
79
98
|
end
|
80
99
|
|
81
100
|
def self.extract hash, path, value, *args, **params
|
82
101
|
slice = (Array === args && args[1].nil? ? (0..-1) : args[1])
|
83
|
-
hash.hash_path_set path => value.scan(args.first)[slice]
|
102
|
+
hash.hash_path_set path => value.to_s.scan(args.first)[slice]
|
84
103
|
end
|
85
104
|
|
86
105
|
def self.extract_first hash, path, value, *args, **params
|
@@ -108,6 +127,23 @@ module BBLib
|
|
108
127
|
hash.hash_path_set path => formatted
|
109
128
|
end
|
110
129
|
|
130
|
+
def self.parse_date_unix hash, path, value, *args, **params
|
131
|
+
format = params.include?(:format) ? params[:format] : '%Y-%m-%d %H:%M:%S'
|
132
|
+
formatted = nil
|
133
|
+
args.each do |pattern|
|
134
|
+
next unless formatted.nil?
|
135
|
+
begin
|
136
|
+
formatted = Time.strptime(value.to_s, pattern.to_s).strftime(format)
|
137
|
+
rescue
|
138
|
+
end
|
139
|
+
end
|
140
|
+
begin
|
141
|
+
if formatted.nil? then formatted = Time.parse(value) end
|
142
|
+
rescue
|
143
|
+
end
|
144
|
+
hash.hash_path_set path => formatted.to_f
|
145
|
+
end
|
146
|
+
|
111
147
|
def self.parse_duration hash, path, value, args, params
|
112
148
|
hash.hash_path_set path => value.to_s.parse_duration(output: args.empty? ? :sec : args )
|
113
149
|
end
|
@@ -155,7 +191,64 @@ module BBLib
|
|
155
191
|
end
|
156
192
|
|
157
193
|
def self.custom hash, path, value, *args, **params
|
158
|
-
|
194
|
+
if params.nil? || params.empty?
|
195
|
+
hash.hash_path_set path => value.send(*args)
|
196
|
+
else
|
197
|
+
hash.hash_path_set path => value.send(*args, **params)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.encapsulate hash, path, value, args, **params
|
202
|
+
hash.hash_path_set path => "#{args}#{value}#{args}"
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.uncapsulate hash, path, value, args, **params
|
206
|
+
value = value[args.size..-1] if value.start_with?(args)
|
207
|
+
value = value[0..-(args.size)-1] if value.end_with?(args)
|
208
|
+
hash.hash_path_set path => value
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.max_number hash, path, value, *args, **params
|
212
|
+
hash.hash_path_set path => value.to_s.extract_numbers.max
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.min_number hash, path, value, *args, **params
|
216
|
+
hash.hash_path_set path => value.to_s.extract_numbers.min
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.avg_number hash, path, value, *args, **params
|
220
|
+
nums = value.to_s.extract_numbers
|
221
|
+
avg = nums.inject{ |s, x| s + x }.to_f / nums.size.to_f
|
222
|
+
hash.hash_path_set path => avg
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.sum_number hash, path, value, *args, **params
|
226
|
+
hash.hash_path_set path => value.to_s.extract_numbers.inject{ |s,x| s + x }
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.strip hash, path, value, args, **params
|
230
|
+
value.map!{ |m| m.respond_to?(:strip) ? m.strip : m } if value.is_a?(Array)
|
231
|
+
hash.hash_path_set path => (value.respond_to?(:strip) ? value.strip : value)
|
232
|
+
end
|
233
|
+
|
234
|
+
def self.extract_integers hash, path, value, args, **params
|
235
|
+
hash.hash_path_set path => (value.extract_integers)
|
236
|
+
end
|
237
|
+
|
238
|
+
def self.extract_floats hash, path, value, args, **params
|
239
|
+
hash.hash_path_set path => (value.extract_floats)
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.extract_numbers hash, path, value, args, **params
|
243
|
+
hash.hash_path_set path => (value.extract_numbers)
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.concat hash, path, value, *args, **params
|
247
|
+
hash.hash_path_set path => "#{value}#{params[:join]}#{hash.hash_path(args.first)[params[:range].nil? ? 0 : params[:range]]}"
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.reverse_concat hash, path, value, *args, **params
|
251
|
+
hash.hash_path_set path => "#{hash.hash_path(args.first)[params[:range].nil? ? 0 : params[:range]]}#{params[:join]}#{value}"
|
159
252
|
end
|
160
253
|
|
161
254
|
end
|
data/lib/string/bbstring.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
|
2
|
-
require_relative 'matching
|
3
|
-
require_relative 'roman
|
4
|
-
require_relative 'fuzzy_matcher
|
2
|
+
require_relative 'matching'
|
3
|
+
require_relative 'roman'
|
4
|
+
require_relative 'fuzzy_matcher'
|
5
|
+
require_relative 'cases'
|
5
6
|
|
6
7
|
module BBLib
|
7
8
|
|
@@ -9,11 +10,7 @@ module BBLib
|
|
9
10
|
# General Functions
|
10
11
|
##############################################
|
11
12
|
|
12
|
-
#
|
13
|
-
# TODO
|
14
|
-
# end
|
15
|
-
|
16
|
-
# Quickly remove any symbols from a string leaving onl alpha-numeric characters and white space.
|
13
|
+
# Quickly remove any symbols from a string leaving only alpha-numeric characters and white space.
|
17
14
|
def self.drop_symbols str
|
18
15
|
str.gsub(/[^\w\s\d]|_/, '')
|
19
16
|
end
|
@@ -28,9 +25,9 @@ module BBLib
|
|
28
25
|
BBLib.extract_numbers(str, convert:false).reject{ |r| !r.include?('.') }.map{ |m| convert ? m.to_f : m }
|
29
26
|
end
|
30
27
|
|
31
|
-
#
|
28
|
+
# Extracts any correctly formed integers or floats from a string
|
32
29
|
def self.extract_numbers str, convert: true
|
33
|
-
str.scan(/\d
|
30
|
+
str.scan(/\d+\.\d+[^\.]|\d+[^\.]/).map{ |f| convert ? (f.include?('.') ? f.to_f : f.to_i) : f }
|
34
31
|
end
|
35
32
|
|
36
33
|
# Used to move the position of the articles 'the', 'a' and 'an' in strings for normalization.
|
@@ -109,12 +106,13 @@ class String
|
|
109
106
|
|
110
107
|
# Simple method to convert a string into an array containing only itself
|
111
108
|
def to_a
|
112
|
-
|
109
|
+
[self]
|
113
110
|
end
|
114
111
|
|
115
112
|
def encap_by? str
|
116
|
-
|
113
|
+
self.start_with?(str) && self.end_with?(str)
|
117
114
|
end
|
115
|
+
|
118
116
|
end
|
119
117
|
|
120
118
|
class Symbol
|
data/lib/string/cases.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module BBLib
|
2
|
+
|
3
|
+
def self.title_case str, first_only: true
|
4
|
+
ignoreables = ['a', 'an', 'the', 'on', 'upon', 'and', 'but', 'or', 'in', 'with', 'to']
|
5
|
+
regx = /[[:space:]]+|\-|\_|\"|\'|\(|\)|\[|\]|\{|\}|\#/
|
6
|
+
spacing = str.scan(regx).to_a
|
7
|
+
words = str.split(regx).map do |word|
|
8
|
+
if ignoreables.include?(word.downcase)
|
9
|
+
word.downcase
|
10
|
+
else
|
11
|
+
if first_only
|
12
|
+
word[0] = word[0].upcase
|
13
|
+
word
|
14
|
+
else
|
15
|
+
word.capitalize
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# Always cap the first word
|
20
|
+
words.first.capitalize
|
21
|
+
words.interleave(spacing).join
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.start_case str, first_only: false
|
25
|
+
regx = /[[:space:]]+|\-|\_|\"|\'|\(|\)|\[|\]|\{|\}|\#/
|
26
|
+
spacing = str.scan(regx).to_a
|
27
|
+
words = str.split(regx).map do |word|
|
28
|
+
if first_only
|
29
|
+
word[0] = word[0].upcase
|
30
|
+
word
|
31
|
+
else
|
32
|
+
word.capitalize
|
33
|
+
end
|
34
|
+
end
|
35
|
+
words.interleave(spacing).join
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.camel_case str, style = :lower
|
39
|
+
regx = /[[:space:]]+|[^[[:alnum:]]]+/
|
40
|
+
words = str.split(regx).map do |word|
|
41
|
+
word.capitalize
|
42
|
+
end
|
43
|
+
words[0].downcase! if style == :lower
|
44
|
+
words.join
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.delimited_case str, delimiter = '_'
|
48
|
+
regx = /[[:space:]]+|[^[[:alnum:]]]+|#{delimiter}+/
|
49
|
+
words = str.split(regx).join(delimiter)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.snake_case str
|
53
|
+
BBLib.delimited_case str, '_'
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.spinal_case str
|
57
|
+
BBLib.delimited_case str, '-'
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.train_case str
|
61
|
+
BBLib.spinal_case(BBLib.start_case(str))
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
class String
|
67
|
+
|
68
|
+
def title_case first_only: false
|
69
|
+
BBLib.title_case self, first_only:first_only
|
70
|
+
end
|
71
|
+
|
72
|
+
def start_case first_only: false
|
73
|
+
BBLib.start_case self, first_only:first_only
|
74
|
+
end
|
75
|
+
|
76
|
+
def camel_case style = :lower
|
77
|
+
BBLib.camel_case self, style
|
78
|
+
end
|
79
|
+
|
80
|
+
def delimited_case delimiter = '_'
|
81
|
+
BBLib.delimited_case self, delimiter
|
82
|
+
end
|
83
|
+
|
84
|
+
def snake_case
|
85
|
+
BBLib.snake_case self
|
86
|
+
end
|
87
|
+
|
88
|
+
def spinal_case
|
89
|
+
BBLib.spinal_case self
|
90
|
+
end
|
91
|
+
|
92
|
+
def train_case
|
93
|
+
BBLib.train_case self
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/lib/string/fuzzy_matcher.rb
CHANGED
@@ -2,20 +2,21 @@
|
|
2
2
|
module BBLib
|
3
3
|
|
4
4
|
class FuzzyMatcher
|
5
|
-
attr_reader :threshold
|
6
|
-
attr_accessor :case_sensitive, :remove_symbols, :move_articles, :convert_roman
|
5
|
+
attr_reader :threshold, :algorithms
|
6
|
+
attr_accessor :case_sensitive, :remove_symbols, :move_articles, :convert_roman, :a, :b
|
7
7
|
|
8
8
|
def initialize threshold: 75, case_sensitive: true, remove_symbols: false, move_articles: false, convert_roman: true
|
9
9
|
self.threshold = threshold
|
10
|
+
setup_algorithms
|
10
11
|
@case_sensitive, @remove_symbols, @move_articles, @convert_roman = case_sensitive, remove_symbols, move_articles, convert_roman
|
11
12
|
end
|
12
13
|
|
13
14
|
# Calculates a percentage match between string a and string b.
|
14
15
|
def similarity a, b
|
15
|
-
return 100.0 if a == b
|
16
16
|
prep_strings a, b
|
17
|
-
|
18
|
-
|
17
|
+
return 100.0 if @a == @b
|
18
|
+
score, total_weight = 0, @algorithms.map{|alg, v| v[:weight] }.inject{ |sum, w| sum+=w }
|
19
|
+
@algorithms.each do |algo, vals|
|
19
20
|
next unless vals[:weight] > 0
|
20
21
|
score+= @a.send(vals[:signature], @b) * vals[:weight]
|
21
22
|
end
|
@@ -44,23 +45,25 @@ module BBLib
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def set_weight algorithm, weight
|
47
|
-
return nil unless
|
48
|
-
|
48
|
+
return nil unless @algorithms.include? algorithm
|
49
|
+
@algorithms[algorithm][:weight] = BBLib.keep_between(weight, 0, nil)
|
49
50
|
end
|
50
51
|
|
51
52
|
def algorithms
|
52
|
-
|
53
|
+
@algorithms.keys
|
53
54
|
end
|
54
55
|
|
55
56
|
private
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
58
|
+
def setup_algorithms
|
59
|
+
@algorithms = {
|
60
|
+
levenshtein: {weight: 10, signature: :levenshtein_similarity},
|
61
|
+
composition: {weight: 5, signature: :composition_similarity},
|
62
|
+
numeric: {weight: 0, signature: :numeric_similarity},
|
63
|
+
phrase: {weight: 0, signature: :phrase_similarity}
|
64
|
+
# FUTURE qwerty: {weight: 0, signature: :qwerty_similarity}
|
65
|
+
}
|
66
|
+
end
|
64
67
|
|
65
68
|
def prep_strings a, b
|
66
69
|
@a, @b = a.to_s.dup.strip, b.to_s.dup.strip
|
data/lib/string/matching.rb
CHANGED
@@ -26,7 +26,7 @@ module BBLib
|
|
26
26
|
# Calculates a percentage based match of two strings based on their character composition.
|
27
27
|
def self.composition_similarity a, b
|
28
28
|
if a.length <= b.length then t = a; a = b; b = t; end
|
29
|
-
matches, temp = 0, b
|
29
|
+
matches, temp = 0, b.dup
|
30
30
|
a.chars.each do |c|
|
31
31
|
if temp.chars.include? c
|
32
32
|
matches+=1
|
@@ -53,7 +53,7 @@ module BBLib
|
|
53
53
|
# Percentage calculations here need to be weighted better...TODO
|
54
54
|
def self.numeric_similarity a, b
|
55
55
|
a, b = a.extract_numbers, b.extract_numbers
|
56
|
-
return 100.0 if a.empty? && b.empty?
|
56
|
+
return 100.0 if a.empty? && b.empty? || a == b
|
57
57
|
matches = []
|
58
58
|
for i in 0..[a.size, b.size].max-1
|
59
59
|
matches << 1.0 / ([a[i].to_f, b[i].to_f].max - [a[i].to_f, b[i].to_f].min + 1.0)
|
data/lib/time/bbtime.rb
CHANGED
@@ -1,10 +1,25 @@
|
|
1
1
|
require_relative 'task_timer'
|
2
|
+
require_relative 'cron'
|
2
3
|
|
3
4
|
module BBLib
|
4
5
|
|
5
6
|
# Parses known time based patterns out of a string to construct a numeric duration.
|
6
|
-
def self.parse_duration str, output: :sec
|
7
|
+
def self.parse_duration str, output: :sec, min_interval: :sec
|
7
8
|
msecs = 0.0
|
9
|
+
|
10
|
+
# Parse time expressions such as 04:05.
|
11
|
+
# The argument min_interval controls what time interval the final number represents
|
12
|
+
str.scan(/\d+\:[\d+\:]+\d+/).each do |e|
|
13
|
+
keys = TIME_EXPS.keys
|
14
|
+
position = keys.index(min_interval)
|
15
|
+
e.split(':').reverse.each do |sec|
|
16
|
+
key = keys[position]
|
17
|
+
msecs+= sec.to_f * TIME_EXPS[key][:mult]
|
18
|
+
position+=1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse expressions such as '1m' or '1 min'
|
8
23
|
TIME_EXPS.each do |k, v|
|
9
24
|
v[:exp].each do |e|
|
10
25
|
numbers = str.downcase.scan(/(?=\w|\D|\A)\d*\.?\d+[[:space:]]*#{e}(?=\W|\d|\z)/i)
|
@@ -13,6 +28,7 @@ module BBLib
|
|
13
28
|
end
|
14
29
|
end
|
15
30
|
end
|
31
|
+
|
16
32
|
msecs / (TIME_EXPS[output][:mult] rescue 1)
|
17
33
|
end
|
18
34
|
|
@@ -108,8 +124,8 @@ module BBLib
|
|
108
124
|
end
|
109
125
|
|
110
126
|
class String
|
111
|
-
def parse_duration output: :sec
|
112
|
-
BBLib.parse_duration self, output:output
|
127
|
+
def parse_duration output: :sec, min_interval: :sec
|
128
|
+
BBLib.parse_duration self, output:output, min_interval:min_interval
|
113
129
|
end
|
114
130
|
end
|
115
131
|
|
data/lib/time/cron.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
module BBLib
|
2
|
+
|
3
|
+
class Cron
|
4
|
+
attr_reader :exp, :parts, :time
|
5
|
+
|
6
|
+
def initialize exp
|
7
|
+
@parts = Hash.new
|
8
|
+
self.exp = exp
|
9
|
+
end
|
10
|
+
|
11
|
+
def closest exp = @exp, direction:1, count: 1, time: Time.now
|
12
|
+
if exp then self.exp = exp end
|
13
|
+
results = []
|
14
|
+
return results unless @exp
|
15
|
+
(1..count).each{ |i| results.push next_time(i == 1 ? time : results.last, direction) }
|
16
|
+
count <= 1 ? results.first : results.reject{ |r| r.nil? }
|
17
|
+
end
|
18
|
+
|
19
|
+
def next exp = @exp, count: 1, time: Time.now
|
20
|
+
closest exp, count:count, time:time, direction:1
|
21
|
+
end
|
22
|
+
|
23
|
+
def prev exp = @exp, count: 1, time: Time.now
|
24
|
+
closest exp, count:count, time:time, direction:-1
|
25
|
+
end
|
26
|
+
|
27
|
+
def exp= e
|
28
|
+
SPECIAL_EXP.each{ |x, v| if v.include?(e) then e = x end }
|
29
|
+
@exp = e
|
30
|
+
parse
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.next exp, count: 1, time: Time.now
|
34
|
+
t = BBLib::Cron.new(exp).next(count:count, time:time)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.prev exp, count: 1, time: Time.now
|
38
|
+
BBLib::Cron.new(exp).prev(count:count, time:time)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.valid? exp
|
42
|
+
!(numeralize(exp) =~ /\A(.*?\s){4,5}.*?\S\z/).nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid? exp
|
46
|
+
BBLib::Cron.valid?(exp)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def parse
|
52
|
+
return nil unless @exp
|
53
|
+
pieces, i = @exp.split(' '), 0
|
54
|
+
PARTS.each do |part, info|
|
55
|
+
@parts[part] = parse_cron_numbers(pieces[i], info[:min], info[:max], Time.now.send(info[:send]))
|
56
|
+
i+=1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.numeralize exp
|
61
|
+
exp = exp.to_s.downcase
|
62
|
+
REPLACE.each do |k, v|
|
63
|
+
v.each do |r|
|
64
|
+
exp.gsub!(r.to_s, k.to_s)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
exp
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_cron_numbers exp, min, max, qmark
|
71
|
+
numbers = Array.new
|
72
|
+
exp = Cron.numeralize(exp)
|
73
|
+
exp.gsub!('?', qmark.to_s)
|
74
|
+
exp.scan(/\*\/\d+|\d+\/\d+|\d+-\d+\/\d+/).each do |s|
|
75
|
+
range, divisor = s.split('/').first, s.split('/').last.to_i
|
76
|
+
if range == '*'
|
77
|
+
range = (min..max)
|
78
|
+
elsif range =~ /\d+\-\d+/
|
79
|
+
range = (range.split('-').first.to_i..range.split('-').last.to_i)
|
80
|
+
else
|
81
|
+
range = (range.to_i..max)
|
82
|
+
end
|
83
|
+
index = 0
|
84
|
+
range.each do |i|
|
85
|
+
if index == 0 || index % divisor.to_i == 0
|
86
|
+
numbers.push i
|
87
|
+
end
|
88
|
+
index+=1
|
89
|
+
end
|
90
|
+
exp.sub!(s, '')
|
91
|
+
end
|
92
|
+
numbers.push exp.scan(/\d+/).map{ |m| m.to_i }
|
93
|
+
exp.strip.scan(/\d+\-\d+/).each do |e|
|
94
|
+
nums = e.scan(/\d+/).map{ |n| n.to_i }
|
95
|
+
numbers.push (nums.min..nums.max).map{ |n| n }
|
96
|
+
end
|
97
|
+
numbers.flatten!.sort!
|
98
|
+
numbers.uniq.reject{ |r| r < min || r > max }
|
99
|
+
end
|
100
|
+
|
101
|
+
def next_day time, direction
|
102
|
+
return nil unless time
|
103
|
+
weekdays, days, months, years = @parts[:weekday], @parts[:day], @parts[:month], @parts[:year]
|
104
|
+
date, safety = nil, 0
|
105
|
+
while date.nil? && safety < 50000
|
106
|
+
if (days.empty? || days.include?(time.day)) && (months.empty? || months.include?(time.month)) && (years.empty? || years.include?(time.year)) && (weekdays.empty? || weekdays.include?(time.wday))
|
107
|
+
date = time
|
108
|
+
else
|
109
|
+
time+= 24*60*60*direction
|
110
|
+
# time = Time.new(time.year, time.month, time.day, 0, 0)
|
111
|
+
end
|
112
|
+
safety+=1
|
113
|
+
end
|
114
|
+
return nil if safety == 50000
|
115
|
+
time
|
116
|
+
end
|
117
|
+
|
118
|
+
def next_time time, direction
|
119
|
+
orig, fw = time.to_f, (direction == 1)
|
120
|
+
current = next_day(time, direction)
|
121
|
+
return nil unless current
|
122
|
+
if (fw ? current.to_f > orig : current.to_f < orig)
|
123
|
+
current = Time.new(current.year, current.month, current.day, (fw ? 0 : 23), (fw ? 0 : 59))
|
124
|
+
else
|
125
|
+
current+= (fw ? 60 : -60)
|
126
|
+
end
|
127
|
+
while !@parts[:day].empty? && !@parts[:day].include?(current.day) || !@parts[:hour].empty? && !@parts[:hour].include?(current.hour) || !@parts[:minute].empty? && !@parts[:minute].include?(current.min)
|
128
|
+
day = [current.day, current.month, current.year]
|
129
|
+
current+= (fw ? 60 : -60)
|
130
|
+
if day != [current.day, current.month, current.year] then current = next_day(current, direction) end
|
131
|
+
return nil unless current
|
132
|
+
end
|
133
|
+
current - current.sec
|
134
|
+
end
|
135
|
+
|
136
|
+
PARTS = {
|
137
|
+
minute: {send: :min, min:0, max:59, size: 60},
|
138
|
+
hour: {send: :hour, min:0, max:23, size: 60*60},
|
139
|
+
day: {send: :day, min:1, max:31, size: 60*60*24},
|
140
|
+
month: {send: :month, min:1, max:12},
|
141
|
+
weekday: {send: :wday, min:0, max:6},
|
142
|
+
year: {send: :year, min:0, max:90000}
|
143
|
+
}
|
144
|
+
|
145
|
+
REPLACE = {
|
146
|
+
1 => [:sunday, :sun, :january, :jan],
|
147
|
+
2 => [:monday, :mon, :february, :feb],
|
148
|
+
3 => [:tuesday, :tues, :tue, :march, :mar],
|
149
|
+
4 => [:wednesday, :wednes, :wed, :april, :apr],
|
150
|
+
5 => [:thursday, :thurs, :thu, :may],
|
151
|
+
6 => [:friday, :fri, :june, :jun],
|
152
|
+
7 => [:saturday, :sat, :july, :jul],
|
153
|
+
8 => [:august, :aug],
|
154
|
+
9 => [:september, :sept, :sep],
|
155
|
+
10 => [:october, :oct],
|
156
|
+
11 => [:november, :nov],
|
157
|
+
12 => [:december, :dec]
|
158
|
+
}
|
159
|
+
|
160
|
+
SPECIAL_EXP = {
|
161
|
+
'0 0 * * * *' => ['@daily', '@midnight', 'daily', 'midnight'],
|
162
|
+
'0 12 * * * *' => ['@noon', 'noon'],
|
163
|
+
'0 0 * * 0 *' => ['@weekly', 'weekly'],
|
164
|
+
'0 0 1 * * *' => ['@monthly', 'monthly'],
|
165
|
+
'0 0 1 1 * *' => ['@yearly', '@annually', 'yearly', 'annually'],
|
166
|
+
'? ? ? ? ? ?' => ['@reboot', '@restart', 'reboot', 'restart']
|
167
|
+
}
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bblib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Black
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-04-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,10 +80,12 @@ files:
|
|
80
80
|
- lib/number/bbnumber.rb
|
81
81
|
- lib/object/bbobject.rb
|
82
82
|
- lib/string/bbstring.rb
|
83
|
+
- lib/string/cases.rb
|
83
84
|
- lib/string/fuzzy_matcher.rb
|
84
85
|
- lib/string/matching.rb
|
85
86
|
- lib/string/roman.rb
|
86
87
|
- lib/time/bbtime.rb
|
88
|
+
- lib/time/cron.rb
|
87
89
|
- lib/time/task_timer.rb
|
88
90
|
homepage: https://github.com/bblack16/bblib-ruby
|
89
91
|
licenses:
|