├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── Gemfile ├── README.md ├── Rakefile ├── RubySeeds.md ├── lib ├── array │ └── convert.rb ├── enumerable │ ├── fraction.rb │ └── group_count.rb ├── hash │ ├── except.rb │ ├── only.rb │ └── stringify_symbolize_keys.rb ├── numeric │ ├── rescale.rb │ └── timespans.rb ├── object │ ├── decompose.rb │ └── hashify.rb └── string │ ├── split2.rb │ └── surround.rb └── spec ├── .rubocop.yml ├── array_spec.rb ├── enumerable_spec.rb ├── hash_spec.rb ├── numeric_spec.rb ├── object_spec.rb └── string_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | .bundle 3 | vendor 4 | Gemfile.lock 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | require: rubocop-rspec 3 | 4 | AllCops: 5 | Include: 6 | - 'lib/**/*' 7 | - 'spec/**/*' 8 | Exclude: 9 | - 'vendor/**/*' 10 | - 'tmp/*' 11 | - Gemfile 12 | - Rakefile 13 | 14 | Layout/SpaceInsideHashLiteralBraces: 15 | EnforcedStyle: no_space 16 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem 'yard' 5 | gem 'rspec', '~> 3' 6 | gem 'rspec-its' 7 | gem 'rubocop' 8 | gem 'rubocop-rspec' 9 | gem 'rake' 10 | end 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RubySeeds: core_ext done right. 2 | 3 | _In the core of apple, there are apple seeds. In your core_ext.rb should 4 | be RubySeeds._ 5 | 6 | It is not unusual for Ruby developers to have own and favourite set of 7 | ruby core classes extensions (typically lying somewhere in project tree in 8 | `core_ext.rb`), dragged from project to project, growing, becoming cluttered 9 | yet beloved. 10 | 11 | Other developers rely on some external thing like ActiveSupport or Ruby 12 | Facets to extend their core classes. And if it is not there, its nowhere. 13 | 14 | RubySeeds is different. 15 | 16 | ## So, what is RubySeeds? 17 | 18 | RubySeeds is **not a gem**. On my strong opinion, there is no good in 19 | mixing together everything somebody can need in core classes. The same 20 | way, there's no good to make a small gem of each useful idea. 21 | 22 | RubySeeds is just an easily navigable [**list of code snippets**](https://github.com/zverok/rubyseeds/blob/master/RubySeeds.md) 23 | with clean and simple explanations. Those snippets are thought to be just 24 | droppable in your projects `lib/core_ext.rb` (with possible renamings 25 | and improvements). 26 | 27 | Snippets selected for RubySeeds posess those common qualities: 28 | * targeting shorter, yet clean code (not "clever" or "magic"); 29 | * as small dependencies between snippets as possible; 30 | * as clean method names as possible; 31 | * snippets' code itself should be small and easily verifieble "by eyes" 32 | (yet tested); 33 | * strong emphasis on "more chainable code", method chaining is cool; 34 | * strong emphasis on data processing with Ruby, so most of RubySeeds do 35 | shorten some container transformations. 36 | 37 | ## Usage 38 | 39 | There's one huge, yet (supposedly) easily-navigable page with all seeds 40 | described: [RubySeeds.md](https://github.com/zverok/rubyseeds/blob/master/RubySeeds.md). 41 | (In future will be separate page with collapsible code.) 42 | 43 | You just browse it, select what you like, expand **Code** section and 44 | copy-paste it into your `core_ext.rb` or wherever you want. 45 | 46 | ### Refinements: highly recommended approach 47 | 48 | Since version 2.0, Ruby has [refinements](http://ruby-doc.org/core-2.1.1/doc/syntax/refinements_rdoc.html) 49 | which allows core (and other) classes to be extended not globally, but in 50 | context of some module. 51 | 52 | Before refinements: 53 | ```ruby 54 | # in core_ext.rb 55 | class Hash 56 | def symbolize_keys 57 | # ... 58 | end 59 | end 60 | 61 | # everywhere in code 62 | require 'core_ext' 63 | # now ANYWHERE Hash#symbolize_keys refers to your implementation... unless 64 | # you require something else, redefining it. 65 | ``` 66 | 67 | With refinements: 68 | ```ruby 69 | # in core_ext.rb 70 | module HashSymbolize 71 | refine Hash do 72 | def symbolize_keys 73 | # ... 74 | end 75 | end 76 | end 77 | 78 | # in some concrete place: 79 | require 'core_ext' 80 | class MyClass 81 | using HashSymbolize 82 | # now Hash#symbolize_keys can be used inside your class only 83 | end 84 | ``` 85 | 86 | ## On "monkey-patching is bad" rant 87 | 88 | Somebody can say that adding functionality to core classess (even through 89 | refinements) is a bad practice. 90 | 91 | It may be (or maybe not) true for large 92 | enterprise-y systems with tens of juniors fixing your code here and there 93 | and some contractor just running through it wildly... 94 | 95 | Yet for many small-to-medium data-processing tasks you just want to be 96 | comfortable with code, having it clean and looking like chain of data 97 | processing calls. So, you just want 98 | `load(something).do_this.and_that.and_shit` instead of multiple 99 | `My::Deeply::Nested::Namespace.very_concrete_processing_method_name(data)` 100 | calls. And you have it. 101 | 102 | Also, take a look at Hal Fultons [post about "monkey patching"](http://rubyhacker.com/blog2/concerning-the-term-monkeypatching.html). 103 | 104 | And [here](http://zverok.github.io/blog/2016-09-28-monkeys.html) is my own latest post on the topic. 105 | 106 | ## On possible name confusion 107 | 108 | There was [rubyseeds](https://github.com/rubyseeds) organization on github, 109 | something about learning Ruby, I guess?.. 110 | 111 | Current project have no relation to this group, and, as 112 | [the site](http://rubyseeds.github.io/materials/) seems to be dead, and 113 | I've already thinked out and loved the name when I found the organization, 114 | I allowed myself to keep the name. 115 | 116 | ## Other nice things to say 117 | 118 | If you are like me (= understand, while we sometimes add features to our 119 | beloved language to make code clear and rational), you may like also those 120 | libraries: 121 | 122 | * [backports](https://github.com/marcandre/backports) or [polyfill](https://github.com/AaronLasseigne/polyfill) 123 | to have all the goodness of new Ruby versions in older ones; 124 | * [hashie](https://github.com/intridea/hashie), which is the most popular 125 | way of treating hashes as a light-weight objects, as well as set of 126 | really useful Hash extensions; 127 | * [naught](https://github.com/avdi/naught) -- a superior Ruby NullObject 128 | pattern implementation, with lot of options; 129 | * [time_math2](https://github.com/zverok/time_math2) (shameless 130 | self-promotion) -- the small and clean library dealing with everyday 131 | `Time` tasks like "next day", "prev day", "todays midnight", 132 | "list of time periods of arbitrary length" and so on (and NO 133 | core_ext-ing this time! compatible with everything). 134 | 135 | ## Contributing 136 | 137 | Just usual fork-change-push-make pull request process. Note (if you haven't 138 | already), that RubySeeds is highly opionated set of extensions, so, 139 | there's no guarantee that pull requests with new features will be accepted. 140 | 141 | NB: never try to update RubySeeds.md file -- it is built automatically from 142 | code in `lib/**/*.rb`, with `rake build`. 143 | 144 | ## License 145 | 146 | Those are code snippets barely dozen lines length. Let's say "public 147 | domain", ok? 148 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc 'Build entire documented file' 2 | task :build do 3 | output = '' 4 | toc = [] 5 | 6 | Dir['lib/**/*.rb'].sort.each do |f| 7 | text = File.read(f).split("\n") 8 | docs = text.take_while{|ln| ln =~ /^\#/}. 9 | map{|ln| ln.sub(/^#\s?/, '').sub(/^\s*Usage:*/, '**Usage:**')} 10 | 11 | code = text.drop_while{|ln| ln =~ /^\#/} 12 | 13 | docs.first =~ /^\#\# / or 14 | fail("#{f}: each file's first line should be markdown header of second level (start with '##')") 15 | 16 | toc << docs.first.sub(/^\#\#\s+/, '') 17 | 18 | output << [ 19 | *docs, 20 | '', 21 | '**Code**', 22 | '```ruby', 23 | *code, 24 | '```', 25 | '' 26 | ].join("\n") 27 | 28 | end 29 | 30 | 31 | File.open('RubySeeds.md', 'w'){|out| 32 | out.puts '# RubySeeds' 33 | out.puts 34 | toc.each do |h| 35 | link = h.downcase.gsub(/[^-a-z0-9 _]/, '').gsub(' ', '-') 36 | out.puts "* [#{h}](##{link})" 37 | end 38 | out.puts 39 | out.write output 40 | } 41 | end 42 | -------------------------------------------------------------------------------- /RubySeeds.md: -------------------------------------------------------------------------------- 1 | # RubySeeds 2 | 3 | * [Array#convert(*conversions) -- per item array processing](#arrayconvertconversions----per-item-array-processing) 4 | * [Enumerable#fraction -- fraction of items in a whole](#enumerablefraction----fraction-of-items-in-a-whole) 5 | * [Enumerable#group_count -- count groups](#enumerablegroup_count----count-groups) 6 | * [Hash#except(*keys)](#hashexceptkeys) 7 | * [Hash#only(*keys)](#hashonlykeys) 8 | * [Hash: stringify and symbolize keys](#hash-stringify-and-symbolize-keys) 9 | * [Numeric: rescale to another range (normalize)](#numeric-rescale-to-another-range-normalize) 10 | * [Numeric: treat as time spans (minutes, hours and so on).](#numeric-treat-as-time-spans-minutes-hours-and-so-on) 11 | * [Object#decompose(:method1, :method2, ...)](#objectdecomposemethod1-method2-) 12 | * [Object#hashify(:method1, :method2, ...)](#objecthashifymethod1-method2-) 13 | * [String#split2(*patterns)](#stringsplit2patterns) 14 | * [String#surround(before, after)](#stringsurroundbefore-after) 15 | 16 | ## Array#convert(*conversions) -- per item array processing 17 | 18 | Processes each item of array with method provided. 19 | 20 | **Usage:** 21 | ```ruby 22 | # Code without convert: 23 | arr = 'test;10;84.0;2015-08-01'.split(';') 24 | return [arr[0], arr[1].to_i, arr[2].to_f, Time.parse(arr[3])] 25 | 26 | # Code with convert: 27 | return 'test;10;84.0;2015-08-01'.split(';'). 28 | convert(:to_s, :to_i, :to_f, Time.method(:parse)) 29 | ``` 30 | Conversions can be: 31 | * symbol (sent to array item); 32 | * proc or method (called with array item as an argument) 33 | * nil (no conversion). 34 | 35 | **Note**: if you'll use this method heavy, you may think of more 36 | complicated implementation, with default values, errors processing and 37 | stuff. But its out of scope of RubySeeds. 38 | 39 | **Code** 40 | ```ruby 41 | class Array 42 | def convert(*conversions) 43 | zip(conversions).map do |val, conv| 44 | case conv 45 | when Symbol then val.send(conv) 46 | when Proc, Method then conv.call(val) 47 | when nil then val 48 | else 49 | raise ArgumentError, 50 | "Can't append conversion of #{conv.class} to #{val.class}" 51 | end 52 | end 53 | end 54 | end 55 | ``` 56 | ## Enumerable#fraction -- fraction of items in a whole 57 | 58 | Counts the part of collection which returns `true` for a block, 59 | relative to entire collection size. 60 | 61 | **Usage:** 62 | 63 | ```ruby 64 | ['test', 'me', 'heck'].fraction { |s| s.include?('t') } # => 1/3 65 | ``` 66 | 67 | 68 | **Code** 69 | ```ruby 70 | module Enumerable 71 | def fraction 72 | whole, frac = inject([0, 0]) { |(w, f), i| [w + 1, f + (yield(i) ? 1 : 0)] } 73 | whole.zero? ? Rational(1) : Rational(frac, whole) 74 | end 75 | end 76 | ``` 77 | ## Enumerable#group_count -- count groups 78 | 79 | Counts items with same value returned by block. 80 | Unlike `#group_by`, when no block provided, counts elements by their 81 | values. 82 | 83 | **Usage:** 84 | 85 | ```ruby 86 | ['test', 'me', 'heck'].group_count(&:length) # => {4 => 2, 2 => 1} 87 | 88 | [:test, :test, :me, :foo, :foo].group_count 89 | # => {:test => 2, :me => 1, :foo => 2} 90 | ``` 91 | 92 | 93 | **Code** 94 | ```ruby 95 | module Enumerable 96 | def group_count(&block) 97 | block ||= ->(x) { x } 98 | Hash.new { 0 }.tap do |res| 99 | each do |val| 100 | res[block.call(val)] += 1 101 | end 102 | end 103 | end 104 | end 105 | ``` 106 | ## Hash#except(*keys) 107 | 108 | Returns the hash without keys specified. 109 | 110 | **Usage:** 111 | ```ruby 112 | {a: 1, b: 2, c: 3, d: 4}.except(:a, :c) # => {b: 2, d: 4} 113 | ``` 114 | 115 | **Code** 116 | ```ruby 117 | class Hash 118 | def except(*keys) 119 | reject { |k, _v| keys.include?(k) } 120 | end 121 | end 122 | ``` 123 | ## Hash#only(*keys) 124 | 125 | Returns the hash, where ONLY the specified keys left. 126 | 127 | **Usage:** 128 | ```ruby 129 | {a: 1, b: 2, c: 3, d: 4}.only(:a, :c) # => {a: 1, c: 3} 130 | ``` 131 | 132 | **Code** 133 | ```ruby 134 | class Hash 135 | def only(*keys) 136 | select { |k, _v| keys.include?(k) } 137 | end 138 | end 139 | ``` 140 | ## Hash: stringify and symbolize keys 141 | 142 | Just standalone implementation of this frequently-used methods. 143 | 144 | **Usage:** 145 | 146 | ```ruby 147 | {test: 1}.stringify_keys # => {'test' => 1} 148 | {'test' => 1}.symbolize_keys # => {:test => 1} 149 | ``` 150 | 151 | Note: Uses just `to_s`/`to_sym` on keys, so, will raise of key is not 152 | convertible. Which is most reasonable thing to do anyways. 153 | 154 | 155 | **Code** 156 | ```ruby 157 | class Hash 158 | def stringify_keys! 159 | keys.each do |key| 160 | self[key.to_s] = delete(key) 161 | end 162 | self 163 | end 164 | 165 | def stringify_keys 166 | dup.stringify_keys! 167 | end 168 | 169 | def symbolize_keys! 170 | keys.each do |key| 171 | self[key.to_sym] = delete(key) 172 | end 173 | self 174 | end 175 | 176 | def symbolize_keys 177 | dup.symbolize_keys! 178 | end 179 | end 180 | ``` 181 | ## Numeric: rescale to another range (normalize) 182 | 183 | Rescales number from one known range to another. 184 | Useful for data normalization. 185 | 186 | **Note**: Depending on usage, you could consider types handling (int/float) 187 | too naive. 188 | 189 | **Usage:** 190 | 191 | ```ruby 192 | array = [1, -5, 84, -3.5, 12] 193 | 194 | array.map{|n| n.rescale(array.min..array.max, 0..100)} 195 | # => [6, 0, 100, 1.68, 19] 196 | 197 | ``` 198 | 199 | **Code** 200 | ```ruby 201 | class Numeric 202 | def rescale(from, to) 203 | from_size = from.end - from.begin 204 | to_size = to.end - to.begin 205 | 206 | (self - from.begin) * to_size / from_size + to.begin 207 | end 208 | end 209 | ``` 210 | ## Numeric: treat as time spans (minutes, hours and so on). 211 | 212 | Really dead simple implementation. Yet it can be useful for small scripts 213 | and prototypes and .irbrc, so I'm having it in my baggage. 214 | 215 | **Note**: implementation is really simple, like `month` is 30 days, `year` 216 | is 365 days, no timezones or stuff. 217 | 218 | **Usage:** 219 | 220 | ```ruby 221 | 10.minutes # 10*60 222 | # seconds, hours, days, weeks, months, years also work 223 | # as well as singular forms like: 224 | 1.hour # => 60*60 225 | ``` 226 | 227 | 228 | **Code** 229 | ```ruby 230 | class Numeric 231 | def seconds 232 | self 233 | end 234 | 235 | def minutes 236 | self * 60 237 | end 238 | 239 | def hours 240 | self * 60.minutes 241 | end 242 | 243 | def days 244 | self * 24.hours 245 | end 246 | 247 | def weeks 248 | self * 7.days 249 | end 250 | 251 | def months 252 | self * 30.days 253 | end 254 | 255 | def years 256 | self * 365.days 257 | end 258 | 259 | alias second seconds 260 | alias minute minutes 261 | alias hour hours 262 | alias day days 263 | alias week weeks 264 | alias month months 265 | alias year years 266 | end 267 | ``` 268 | ## Object#decompose(:method1, :method2, ...) 269 | 270 | "Decomposes" some object into array of values, received by calling its 271 | getters. 272 | 273 | **Usage:** 274 | ```ruby 275 | # assuming we have some Author model 276 | Author.first.decompose(:first_name, :last_name).join(' ') 277 | ``` 278 | 279 | 280 | **Code** 281 | ```ruby 282 | class Object 283 | def decompose(*methods) 284 | methods.map { |m| public_send(m) } 285 | end 286 | end 287 | ``` 288 | ## Object#hashify(:method1, :method2, ...) 289 | 290 | Converts object to hash of selected attributes 291 | 292 | **Usage:** 293 | 294 | ```ruby 295 | # assuming we have some Author model 296 | Author.first.hashify(:first_name, :last_name) 297 | # => {first_name: 'John', last_name: 'Lennon'} 298 | ``` 299 | 300 | 301 | **Code** 302 | ```ruby 303 | class Object 304 | def hashify(*methods) 305 | methods.map { |m| [m, public_send(m)] }.to_h 306 | end 307 | end 308 | ``` 309 | ## String#split2(*patterns) 310 | 311 | Recursively split string by multiple patterns. 312 | 313 | **Usage:** 314 | ```ruby 315 | # Without split2 316 | 'word:10,other:20,stats:50'.split(',').map{|wc| wc.split(':')} 317 | # => [['word', '10'], ['other', '20'], ['stats', '50']] 318 | File.read('data.tsv').split("\n").map{|ln| ln.split("\t")} 319 | # => array of arrays 320 | 321 | # With split2 322 | 'word:10,other:20,stats:50'.split2(',', ':') 323 | # => [['word', '10'], ['other', '20'], ['stats', '50']] 324 | File.read('data.tsv').split2("\n", "\t") 325 | # => array of arrays 326 | ``` 327 | 328 | Note: there can be any number of patterns, not only two. 329 | 330 | **Code** 331 | ```ruby 332 | class String 333 | def split2(*patterns) 334 | if patterns.count > 1 335 | split(patterns.first).map { |item| item.split2(*patterns[1..-1]) } 336 | else 337 | split(patterns.first) 338 | end 339 | end 340 | end 341 | ``` 342 | ## String#surround(before, after) 343 | 344 | Surrounds string with before/after strings. 345 | 346 | **Usage:** 347 | ```ruby 348 | 'test'.surround('(', ')') # => '(test)' 349 | 'test'.surround('|') # => '|test|' 350 | ``` 351 | 352 | Note: for symmetry, some could ask for `#append` and `#prepend`... but 353 | I've never felt a need for this. 354 | 355 | **Code** 356 | ```ruby 357 | class String 358 | def surround(before, after = before) 359 | "#{before}#{self}#{after}" 360 | end 361 | end 362 | ``` 363 | -------------------------------------------------------------------------------- /lib/array/convert.rb: -------------------------------------------------------------------------------- 1 | # ## Array#convert(*conversions) -- per item array processing 2 | # 3 | # Processes each item of array with method provided. 4 | # 5 | # Usage: 6 | # ```ruby 7 | # # Code without convert: 8 | # arr = 'test;10;84.0;2015-08-01'.split(';') 9 | # return [arr[0], arr[1].to_i, arr[2].to_f, Time.parse(arr[3])] 10 | # 11 | # # Code with convert: 12 | # return 'test;10;84.0;2015-08-01'.split(';'). 13 | # convert(:to_s, :to_i, :to_f, Time.method(:parse)) 14 | # ``` 15 | # Conversions can be: 16 | # * symbol (sent to array item); 17 | # * proc or method (called with array item as an argument) 18 | # * nil (no conversion). 19 | # 20 | # **Note**: if you'll use this method heavy, you may think of more 21 | # complicated implementation, with default values, errors processing and 22 | # stuff. But its out of scope of RubySeeds. 23 | class Array 24 | def convert(*conversions) 25 | zip(conversions).map do |val, conv| 26 | case conv 27 | when Symbol then val.send(conv) 28 | when Proc, Method then conv.call(val) 29 | when nil then val 30 | else 31 | raise ArgumentError, 32 | "Can't append conversion of #{conv.class} to #{val.class}" 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/enumerable/fraction.rb: -------------------------------------------------------------------------------- 1 | # ## Enumerable#fraction -- fraction of items in a whole 2 | # 3 | # Counts the part of collection which returns `true` for a block, 4 | # relative to entire collection size. 5 | # 6 | # Usage: 7 | # 8 | # ```ruby 9 | # ['test', 'me', 'heck'].fraction { |s| s.include?('t') } # => 1/3 10 | # ``` 11 | # 12 | module Enumerable 13 | def fraction 14 | whole, frac = inject([0, 0]) { |(w, f), i| [w + 1, f + (yield(i) ? 1 : 0)] } 15 | whole.zero? ? Rational(1) : Rational(frac, whole) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/enumerable/group_count.rb: -------------------------------------------------------------------------------- 1 | # ## Enumerable#group_count -- count groups 2 | # 3 | # Counts items with same value returned by block. 4 | # Unlike `#group_by`, when no block provided, counts elements by their 5 | # values. 6 | # 7 | # Usage: 8 | # 9 | # ```ruby 10 | # ['test', 'me', 'heck'].group_count(&:length) # => {4 => 2, 2 => 1} 11 | # 12 | # [:test, :test, :me, :foo, :foo].group_count 13 | # # => {:test => 2, :me => 1, :foo => 2} 14 | # ``` 15 | # 16 | module Enumerable 17 | def group_count(&block) 18 | block ||= ->(x) { x } 19 | Hash.new { 0 }.tap do |res| 20 | each do |val| 21 | res[block.call(val)] += 1 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hash/except.rb: -------------------------------------------------------------------------------- 1 | # ## Hash#except(*keys) 2 | # 3 | # Returns the hash without keys specified. 4 | # 5 | # Usage: 6 | # ```ruby 7 | # {a: 1, b: 2, c: 3, d: 4}.except(:a, :c) # => {b: 2, d: 4} 8 | # ``` 9 | class Hash 10 | def except(*keys) 11 | reject { |k, _v| keys.include?(k) } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/hash/only.rb: -------------------------------------------------------------------------------- 1 | # ## Hash#only(*keys) 2 | # 3 | # Returns the hash, where ONLY the specified keys left. 4 | # 5 | # Usage: 6 | # ```ruby 7 | # {a: 1, b: 2, c: 3, d: 4}.only(:a, :c) # => {a: 1, c: 3} 8 | # ``` 9 | class Hash 10 | def only(*keys) 11 | select { |k, _v| keys.include?(k) } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/hash/stringify_symbolize_keys.rb: -------------------------------------------------------------------------------- 1 | # ## Hash: stringify and symbolize keys 2 | # 3 | # Just standalone implementation of this frequently-used methods. 4 | # 5 | # Usage: 6 | # 7 | # ```ruby 8 | # {test: 1}.stringify_keys # => {'test' => 1} 9 | # {'test' => 1}.symbolize_keys # => {:test => 1} 10 | # ``` 11 | # 12 | # Note: Uses just `to_s`/`to_sym` on keys, so, will raise of key is not 13 | # convertible. Which is most reasonable thing to do anyways. 14 | # 15 | class Hash 16 | def stringify_keys! 17 | keys.each do |key| 18 | self[key.to_s] = delete(key) 19 | end 20 | self 21 | end 22 | 23 | def stringify_keys 24 | dup.stringify_keys! 25 | end 26 | 27 | def symbolize_keys! 28 | keys.each do |key| 29 | self[key.to_sym] = delete(key) 30 | end 31 | self 32 | end 33 | 34 | def symbolize_keys 35 | dup.symbolize_keys! 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/numeric/rescale.rb: -------------------------------------------------------------------------------- 1 | # ## Numeric: rescale to another range (normalize) 2 | # 3 | # Rescales number from one known range to another. 4 | # Useful for data normalization. 5 | # 6 | # **Note**: Depending on usage, you could consider types handling (int/float) 7 | # too naive. 8 | # 9 | # Usage: 10 | # 11 | # ```ruby 12 | # array = [1, -5, 84, -3.5, 12] 13 | # 14 | # array.map{|n| n.rescale(array.min..array.max, 0..100)} 15 | # # => [6, 0, 100, 1.68, 19] 16 | # 17 | # ``` 18 | class Numeric 19 | def rescale(from, to) 20 | from_size = from.end - from.begin 21 | to_size = to.end - to.begin 22 | 23 | (self - from.begin) * to_size / from_size + to.begin 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/numeric/timespans.rb: -------------------------------------------------------------------------------- 1 | # ## Numeric: treat as time spans (minutes, hours and so on). 2 | # 3 | # Really dead simple implementation. Yet it can be useful for small scripts 4 | # and prototypes and .irbrc, so I'm having it in my baggage. 5 | # 6 | # **Note**: implementation is really simple, like `month` is 30 days, `year` 7 | # is 365 days, no timezones or stuff. 8 | # 9 | # Usage: 10 | # 11 | # ```ruby 12 | # 10.minutes # 10*60 13 | # # seconds, hours, days, weeks, months, years also work 14 | # # as well as singular forms like: 15 | # 1.hour # => 60*60 16 | # ``` 17 | # 18 | class Numeric 19 | def seconds 20 | self 21 | end 22 | 23 | def minutes 24 | self * 60 25 | end 26 | 27 | def hours 28 | self * 60.minutes 29 | end 30 | 31 | def days 32 | self * 24.hours 33 | end 34 | 35 | def weeks 36 | self * 7.days 37 | end 38 | 39 | def months 40 | self * 30.days 41 | end 42 | 43 | def years 44 | self * 365.days 45 | end 46 | 47 | alias second seconds 48 | alias minute minutes 49 | alias hour hours 50 | alias day days 51 | alias week weeks 52 | alias month months 53 | alias year years 54 | end 55 | -------------------------------------------------------------------------------- /lib/object/decompose.rb: -------------------------------------------------------------------------------- 1 | # ## Object#decompose(:method1, :method2, ...) 2 | # 3 | # "Decomposes" some object into array of values, received by calling its 4 | # getters. 5 | # 6 | # Usage: 7 | # ```ruby 8 | # # assuming we have some Author model 9 | # Author.first.decompose(:first_name, :last_name).join(' ') 10 | # ``` 11 | # 12 | class Object 13 | def decompose(*methods) 14 | methods.map { |m| public_send(m) } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/object/hashify.rb: -------------------------------------------------------------------------------- 1 | # ## Object#hashify(:method1, :method2, ...) 2 | # 3 | # Converts object to hash of selected attributes 4 | # 5 | # Usage: 6 | # 7 | # ```ruby 8 | # # assuming we have some Author model 9 | # Author.first.hashify(:first_name, :last_name) 10 | # # => {first_name: 'John', last_name: 'Lennon'} 11 | # ``` 12 | # 13 | class Object 14 | def hashify(*methods) 15 | methods.map { |m| [m, public_send(m)] }.to_h 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/string/split2.rb: -------------------------------------------------------------------------------- 1 | # ## String#split2(*patterns) 2 | # 3 | # Recursively split string by multiple patterns. 4 | # 5 | # Usage: 6 | # ```ruby 7 | # # Without split2 8 | # 'word:10,other:20,stats:50'.split(',').map{|wc| wc.split(':')} 9 | # # => [['word', '10'], ['other', '20'], ['stats', '50']] 10 | # File.read('data.tsv').split("\n").map{|ln| ln.split("\t")} 11 | # # => array of arrays 12 | # 13 | # # With split2 14 | # 'word:10,other:20,stats:50'.split2(',', ':') 15 | # # => [['word', '10'], ['other', '20'], ['stats', '50']] 16 | # File.read('data.tsv').split2("\n", "\t") 17 | # # => array of arrays 18 | # ``` 19 | # 20 | # Note: there can be any number of patterns, not only two. 21 | class String 22 | def split2(*patterns) 23 | if patterns.count > 1 24 | split(patterns.first).map { |item| item.split2(*patterns[1..-1]) } 25 | else 26 | split(patterns.first) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/string/surround.rb: -------------------------------------------------------------------------------- 1 | # ## String#surround(before, after) 2 | # 3 | # Surrounds string with before/after strings. 4 | # 5 | # Usage: 6 | # ```ruby 7 | # 'test'.surround('(', ')') # => '(test)' 8 | # 'test'.surround('|') # => '|test|' 9 | # ``` 10 | # 11 | # Note: for symmetry, some could ask for `#append` and `#prepend`... but 12 | # I've never felt a need for this. 13 | class String 14 | def surround(before, after = before) 15 | "#{before}#{self}#{after}" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ../.rubocop.yml 2 | 3 | Metrics/BlockLength: 4 | Enabled: false 5 | 6 | Metrics/LineLength: 7 | Max: 100 8 | 9 | RSpec/ExampleLength: 10 | Enabled: false 11 | 12 | RSpec/MultipleExpectations: 13 | Enabled: false 14 | 15 | -------------------------------------------------------------------------------- /spec/array_spec.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/array/*.rb'].shuffle.each { |f| require f } 2 | require 'time' 3 | 4 | describe Array do 5 | describe '#convert' do 6 | it 'converts!' do 7 | expect(['test', '1', '2015-03-01'].convert(nil, :to_i, Time.method(:parse))) 8 | .to eq ['test', 1, Time.local(2015, 3, 1)] 9 | end 10 | 11 | it 'just leaves extra elements' do 12 | expect(['test', '1', '2015-03-01'].convert(nil, :to_i)) 13 | .to eq ['test', 1, '2015-03-01'] 14 | end 15 | 16 | it 'ignores extra methods' do 17 | expect(%w[test 1].convert(nil, :to_i, Time.method(:parse))) 18 | .to eq ['test', 1] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/enumerable_spec.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/enumerable/*.rb'].shuffle.each { |f| require f } 2 | 3 | describe Enumerable do 4 | describe '#group_count' do 5 | it 'counts with block' do 6 | expect([1, 1, 2, 2, 2, 3, 4, 4, 4, 5].group_count(&:odd?)) 7 | .to eq(false => 6, true => 4) 8 | end 9 | 10 | it 'counts without block' do 11 | expect([1, 1, 2, 2, 2, 3, 4, 4, 4, 5].group_count) 12 | .to eq(1 => 2, 2 => 3, 3 => 1, 4 => 3, 5 => 1) 13 | end 14 | end 15 | 16 | describe '#fraction' do 17 | it 'counts non-empty fraction' do 18 | expect((1..30).fraction(&:odd?)).to eq Rational(1, 2) 19 | expect((1..30).fraction { |i| i.to_s.include?('3') }).to eq Rational(2, 15) 20 | expect((1..30).fraction { |i| !i.zero? }).to eq 1 21 | end 22 | 23 | it 'counts empty fraction' do 24 | expect((1..30).fraction(&:nil?)).to eq 0 25 | end 26 | 27 | it 'works with empty array' do 28 | expect([].fraction(&:nil?)).to eq 1 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/hash_spec.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/hash/*.rb'].shuffle.each { |f| require f } 2 | 3 | describe Hash do 4 | describe '#symbolize_keys!' do 5 | it 'symbolizes' do 6 | hash = {first: 1, 'second' => 2} 7 | hash.symbolize_keys! 8 | expect(hash).to eq(first: 1, second: 2) 9 | end 10 | 11 | it 'raises on non-symbolizable' do 12 | hash = {'first' => 1, 2 => 2} 13 | expect { hash.symbolize_keys! }.to raise_error(NoMethodError, /to_sym/) 14 | end 15 | end 16 | 17 | describe '#stringify_keys!' do 18 | it 'stringifies' do 19 | hash = {first: 1, 'second' => 2} 20 | hash.stringify_keys! 21 | expect(hash).to eq('first' => 1, 'second' => 2) 22 | end 23 | end 24 | 25 | describe '#except' do 26 | it 'drops keys' do 27 | expect({first: 1, second: 2, third: 3}.except(:first, :third)) 28 | .to eq(second: 2) 29 | end 30 | end 31 | 32 | describe '#only' do 33 | it 'preserves keys' do 34 | expect({first: 1, second: 2, third: 3}.only(:first, :third)) 35 | .to eq(first: 1, third: 3) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/numeric_spec.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/numeric/*.rb'].shuffle.each { |f| require f } 2 | 3 | describe Numeric do 4 | describe 'timespans' do 5 | it 'converts' do 6 | expect(10.seconds).to eq 10 7 | expect(10.minutes).to eq 10 * 60 8 | expect(10.hours).to eq 10 * 60 * 60 9 | expect(10.days).to eq 10 * 60 * 60 * 24 10 | expect(10.weeks).to eq 10 * 60 * 60 * 24 * 7 11 | expect(10.months).to eq 10 * 60 * 60 * 24 * 30 12 | expect(10.years).to eq 10 * 60 * 60 * 24 * 365 13 | end 14 | end 15 | 16 | describe '#rescale' do 17 | it 'rescales' do 18 | expect(5.rescale(0..10, -100..100)).to eq 0 19 | expect(0.3.rescale(0..1, 10..20)).to be_within(0.01).of(13) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/object_spec.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/object/*.rb'].shuffle.each { |f| require f } 2 | 3 | describe Object do 4 | describe '#decompose' do 5 | it 'decomposes any objects into set of values' do 6 | expect('Test Me'.decompose(:upcase, :downcase, :length)) 7 | .to eq(['TEST ME', 'test me', 7]) 8 | end 9 | 10 | it 'fails on unknown methods' do 11 | expect { 'Test Me'.decompose(:upscase) }.to raise_error(NoMethodError) 12 | end 13 | 14 | it 'fails on private methods' do 15 | expect { 'Test Me'.decompose(:puts) }.to raise_error(NoMethodError) 16 | end 17 | end 18 | 19 | describe '#hashify' do 20 | it 'decomposes any objects into hash of values' do 21 | expect('Test Me'.hashify(:upcase, :downcase, :length)) 22 | .to eq(upcase: 'TEST ME', downcase: 'test me', length: 7) 23 | end 24 | 25 | it 'fails on unknown methods' do 26 | expect { 'Test Me'.hashify(:upscase) }.to raise_error(NoMethodError) 27 | end 28 | 29 | it 'fails on private methods' do 30 | expect { 'Test Me'.hashify(:puts) }.to raise_error(NoMethodError) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/string_spec.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/string/*.rb'].shuffle.each { |f| require f } 2 | 3 | describe String do 4 | describe '#split2' do 5 | it 'splits!' do 6 | expect('foo:1,bar:2,heck:3'.split2(',', ':')) 7 | .to eq [%w[foo 1], %w[bar 2], %w[heck 3]] 8 | end 9 | end 10 | 11 | describe '#surround' do 12 | it 'surrounds?' do 13 | expect('test'.surround('(', ')')).to eq '(test)' 14 | expect('test'.surround('|')).to eq '|test|' 15 | end 16 | end 17 | end 18 | --------------------------------------------------------------------------------