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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 46227889fa2341b852f07f10dd28892a0770b759
4
- data.tar.gz: f7e8f2dae0ba5148cb77d0ec7086c87806dce3c7
3
+ metadata.gz: d6ed599d59ab2bd4d0a8cdae4cd61383e23d4e64
4
+ data.tar.gz: 2a528e9bcdb00c34514420da98cb1f990996f9d6
5
5
  SHA512:
6
- metadata.gz: 45f56290f00c86f5ead9f13746fe76104d87698bfd8b99a89abc2fc1660b4dc4137fb0bb85d50ed9dc8bfcfb0e987b1bef83e41e40c9599eb2205b2809a0cc7a
7
- data.tar.gz: 50f0e4937c9857b0f66c14b748a74fec84bd835da790e01c64774005e7d9f9f24a58e0e96f2204cfcaf65a690e1ae5ce0d3eec40c0b76e9a25623f1824f6fd60
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. Currently the library is in an early state and is being written generally for education purposes. As such, large changes will likely occur and some functions may be incomplete or inaccurate until 1.0.
3
+ BBLib (Brandon-Black-Lib) is a collection of various reusable methods and classes to extend the Ruby language.
4
4
 
5
- One of my primary goals with the core BBLib code is to keep it as lightweight as possible. This means you will not find dependencies outside of the Ruby core libraries in this code. Further modules that do have larger dependencies will be released in separate gems.
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. Similarily, if :dir is passed to _scan_dirs_ a ruby Dir object is returned.
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 **!** version of _deep_merge_ is also available to modify the hash in place rather than returning a new hash.
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 inplace (**!**) version of the method is also available.
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 delim. The string is split be each delimiter in order and an Array is returned.
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 return Array are removed. This behavior can be changed using the _:keep_empty_ named param.
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
- **drop_symbols**
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
- A simple method to remove all non-alpha, non-numeric and non-whitespace characters from a string. Extended to the String class.
477
+ puts cron.previous
478
+ #=> 2016-04-03 21:59:00 -0600
417
479
 
418
- **extract_integers**
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
- Returns an array of all integers found within a string. The named param _:convert_ can be set to true to convert the extract numbers into Fixnums. If left false, strings are returned instead.
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
- **extract_floats**
488
+ An instantiated Cron object is not necessary to get the next and previous times.
423
489
 
424
- Performs the same action as _extract_integers_ except it can also pull floats from a string. The _:convert_ param is also available, but converts the strings into floats.
490
+ ```ruby
491
+ puts BBLib::Cron.next('* * * * * *')
492
+ #=> 2016-04-03 22:04:00 -0600
425
493
 
426
- **extract_numbers**
494
+ puts BBLib::Cron.next('0-5 * * * * *')
495
+ #=> 2016-04-03 22:04:00 -0600
427
496
 
428
- See above. Is an alias for _extract_floats_.
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
- ### Time
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
- * :mili
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:).
@@ -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
@@ -1,3 +1,3 @@
1
1
  module BBLib
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -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
- FILE_SIZES = {
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
@@ -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
@@ -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
- matches.push hash.dig(patterns).flatten(1)[p[:slice]]
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 matches << hash[p[:slice]] end
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
- matches.push hash.values.flatten(1)[p[:slice]]
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.flatten(1)
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
- exists = (details[:symbol_sensitive] ? h.include?(d[:last][:key]) : (h.include?(d[:last][:key].to_sym) || h.include?(d[:last][:key].to_s) ))
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
- if !slice.is_a?(Range) && !slice.is_a?(Fixnum) then slice = (0..-1) end
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 == delimiter && !open then paths << path[0..stop].reverse.sub(delimiter,'').reverse; path = path[stop+1..-1]; stop = -1 end
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:true}
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|
@@ -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
- value = hash.hash_path(path).first
16
- if params.include?(:condition) && params[:condition]
17
- begin
18
- next unless eval(params[:condition].gsub('$', value.to_s))
19
- rescue
20
- next
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
- # concat: { aliases: [:join, :concat_with]},
53
- # reverse_concat: { aliases: [:reverse_join, :reverse_concat_with]}
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.to_s, v.to_s) }
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
- hash.hash_path_set path => value.send(*args)
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
@@ -1,7 +1,8 @@
1
1
 
2
- require_relative 'matching.rb'
3
- require_relative 'roman.rb'
4
- require_relative 'fuzzy_matcher.rb'
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
- # def self.title_case str
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
- # Alias for extract_floats
28
+ # Extracts any correctly formed integers or floats from a string
32
29
  def self.extract_numbers str, convert: true
33
- str.scan(/\d+\.?\d+|\d+/).map{ |f| convert ? (f.include?('.') ? f.to_f : f.to_i) : f }
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
- return [self]
109
+ [self]
113
110
  end
114
111
 
115
112
  def encap_by? str
116
- return self.start_with?(str) && self.end_with?(str)
113
+ self.start_with?(str) && self.end_with?(str)
117
114
  end
115
+
118
116
  end
119
117
 
120
118
  class Symbol
@@ -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
@@ -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
- score, total_weight = 0, ALGORITHMS.map{|a, v| v[:weight] }.inject{ |sum, w| sum+=w }
18
- ALGORITHMS.each do |algo, vals|
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 ALGORITHMS.include? algorithm
48
- ALGORITHMS[algorithm] = BBLib.keep_between(weight, 0, nil)
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
- ALGORITHMS.keys
53
+ @algorithms.keys
53
54
  end
54
55
 
55
56
  private
56
57
 
57
- ALGORITHMS = {
58
- levenshtein: {weight: 10, signature: :levenshtein_similarity},
59
- composition: {weight: 5, signature: :composition_similarity},
60
- numeric: {weight: 0, signature: :numeric_similarity},
61
- phrase: {weight: 0, signature: :phrase_similarity}
62
- # FUTURE qwerty: {weight: 0, signature: :qwerty_similarity}
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
@@ -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)
@@ -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
 
@@ -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.0
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-02-12 00:00:00.000000000 Z
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: