fakeredis 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 88195a8e3e41a553347ff30096fac78a8dcec153
4
- data.tar.gz: 3ea4aa2ec9ec9857486a503ef8032b2f22562a60
3
+ metadata.gz: 20688b94f039d268518697ede7cdfad15f26cf73
4
+ data.tar.gz: ab1982d7e9ed2f7c6b864d2beadd63d6041bb59a
5
5
  SHA512:
6
- metadata.gz: 28fd4cbbe0ec660365112a7b7485c950771a4c8d75c933d0d55afdb3cfbdabdd1fdeba98d687e9050b01fdf2e52e07278b8497a8d0e03ac97f974c0211afaa72
7
- data.tar.gz: 0c5c519761c03545a014d0822c3c4a32d63ea6ae3bd96f7c7b6e433dd5acf646dec8b195b5675838cd56e3ac6e5320164654318724e8c3e57ee8f53e32a408cd
6
+ metadata.gz: 080b44a262bae01a09dcd2660cb35e1a52543cad7d3780fc58224854b6007adb8a0598a0b892560988f7a86c921a1ea1d3b36fa19248542017ffb97ce8caa0ef
7
+ data.tar.gz: 687896c96f26f389776216ef6397ce9bb025e9fb04008860dc377f8ba0965245185b02bb4381e7e560a5d76621b8c87ee4989878fbe0bd086e5fc30e2e3c6442
@@ -1,8 +1,8 @@
1
+ language: ruby
1
2
  rvm:
2
- - 1.8.7
3
3
  - 1.9.2
4
4
  - 1.9.3
5
5
  - 2.0.0
6
- - ree
7
- - jruby
8
- - rbx
6
+ - 2.1.1
7
+ - jruby-19mode
8
+ - rbx-2
data/Gemfile CHANGED
@@ -1,9 +1,13 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem 'rake'
4
4
  gem 'rdoc'
5
5
 
6
- gem 'rubysl', :platforms => :rbx
6
+ platforms :rbx do
7
+ gem 'racc'
8
+ gem 'rubysl', '~> 2.0'
9
+ gem 'psych'
10
+ end
7
11
 
8
12
  # Specify your gem's dependencies in fakeredis.gemspec
9
13
  gemspec
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2013 Guillermo Iguaran
1
+ Copyright (c) 2011-2014 Guillermo Iguaran
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -15,7 +15,7 @@ Add it to your Gemfile:
15
15
 
16
16
  ## Versions
17
17
 
18
- FakeRedis currently supports redis-rb v3.0.0 or later, if you are using
18
+ FakeRedis currently supports redis-rb v3.x.y or later, if you are using
19
19
  redis-rb v2.2.x install the version 0.3.x:
20
20
 
21
21
  gem install fakeredis -v "~> 0.3.0"
@@ -82,5 +82,5 @@ Or:
82
82
 
83
83
  ## Copyright
84
84
 
85
- Copyright (c) 2011-2013 Guillermo Iguaran. See LICENSE for
85
+ Copyright (c) 2011-2014 Guillermo Iguaran. See LICENSE for
86
86
  further details.
@@ -18,6 +18,6 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_runtime_dependency(%q<redis>, ["~> 3.0.0"])
22
- s.add_development_dependency(%q<rspec>, [">= 2.0.0"])
21
+ s.add_runtime_dependency(%q<redis>, ["~> 3.0"])
22
+ s.add_development_dependency(%q<rspec>, ["~> 3.0"])
23
23
  end
@@ -0,0 +1,25 @@
1
+ module FakeRedis
2
+ module CommandExecutor
3
+ def write(command)
4
+ meffod = command.shift.to_s.downcase.to_sym
5
+
6
+ if in_multi && !(TRANSACTION_COMMANDS.include? meffod) # queue commands
7
+ queued_commands << [meffod, *command]
8
+ reply = 'QUEUED'
9
+ elsif respond_to?(meffod)
10
+ reply = send(meffod, *command)
11
+ else
12
+ raise Redis::CommandError, "ERR unknown command '#{meffod}'"
13
+ end
14
+
15
+ if reply == true
16
+ reply = 1
17
+ elsif reply == false
18
+ reply = 0
19
+ end
20
+
21
+ replies << reply
22
+ nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,116 @@
1
+ # Codes are mostly referenced from MockRedis' implementation.
2
+ module FakeRedis
3
+ module SortMethod
4
+ def sort(key, *redis_options_array)
5
+ return [] unless key
6
+
7
+ unless %w(list set zset).include? type(key)
8
+ warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
9
+ raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
10
+ end
11
+
12
+ # redis_options is an array of format [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]
13
+ # Lets nibble it back into a hash
14
+ options = extract_options_from(redis_options_array)
15
+
16
+ # And now to actually do the work of this method
17
+
18
+ projected = project(data[key], options[:by], options[:get])
19
+ sorted = sort_by(projected, options[:order])
20
+ sliced = slice(sorted, options[:limit])
21
+ # We have to flatten it down as redis-rb adds back the array to the return value
22
+ result = sliced.flatten(1)
23
+
24
+ options[:store] ? rpush(options[:store], sliced) : sliced.flatten(1)
25
+ end
26
+
27
+ private
28
+
29
+ ASCENDING_SORT = Proc.new { |a, b| a.first <=> b.first }
30
+ DESCENDING_SORT = Proc.new { |a, b| b.first <=> a.first }
31
+
32
+ def extract_options_from(options_array)
33
+ # Defaults
34
+ options = {
35
+ :limit => [],
36
+ :order => "ASC",
37
+ :get => []
38
+ }
39
+
40
+ if options_array.first == "BY"
41
+ options_array.shift
42
+ options[:by] = options_array.shift
43
+ end
44
+
45
+ if options_array.first == "LIMIT"
46
+ options_array.shift
47
+ options[:limit] = [options_array.shift, options_array.shift]
48
+ end
49
+
50
+ while options_array.first == "GET"
51
+ options_array.shift
52
+ options[:get] << options_array.shift
53
+ end
54
+
55
+ if %w(ASC DESC ALPHA).include?(options_array.first)
56
+ options[:order] = options_array.shift
57
+ options[:order] = "ASC" if options[:order] == "ALPHA"
58
+ end
59
+
60
+ if options_array.first == "STORE"
61
+ options_array.shift
62
+ options[:store] = options_array.shift
63
+ end
64
+
65
+ options
66
+ end
67
+
68
+ def project(enumerable, by, get_patterns)
69
+ enumerable.map do |*elements|
70
+ element = elements.flatten.first
71
+ weight = by ? lookup_from_pattern(by, element) : element
72
+ value = element
73
+
74
+ if get_patterns.length > 0
75
+ value = get_patterns.map do |pattern|
76
+ pattern == "#" ? element : lookup_from_pattern(pattern, element)
77
+ end
78
+ value = value.first if value.length == 1
79
+ end
80
+
81
+ [weight, value]
82
+ end
83
+ end
84
+
85
+ def sort_by(projected, direction)
86
+ sorter =
87
+ case direction.upcase
88
+ when "DESC"
89
+ DESCENDING_SORT
90
+ when "ASC", "ALPHA"
91
+ ASCENDING_SORT
92
+ else
93
+ raise "Invalid direction '#{direction}'"
94
+ end
95
+
96
+ projected.sort(&sorter).map(&:last)
97
+ end
98
+
99
+ def slice(sorted, limit)
100
+ skip = limit.first || 0
101
+ take = limit.last || sorted.length
102
+
103
+ sorted[skip...(skip + take)] || sorted
104
+ end
105
+
106
+ def lookup_from_pattern(pattern, element)
107
+ key = pattern.sub('*', element)
108
+
109
+ if (hash_parts = key.split('->')).length > 1
110
+ hget hash_parts.first, hash_parts.last
111
+ else
112
+ get key
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,83 @@
1
+ module FakeRedis
2
+ TRANSACTION_COMMANDS = [:discard, :exec, :multi, :watch, :unwatch]
3
+
4
+ module TransactionCommands
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ def self.queued_commands
8
+ @queued_commands ||= Hash.new {|h,k| h[k] = [] }
9
+ end
10
+
11
+ def self.in_multi
12
+ @in_multi ||= Hash.new{|h,k| h[k] = false}
13
+ end
14
+
15
+ def queued_commands
16
+ self.class.queued_commands[database_instance_key]
17
+ end
18
+
19
+ def queued_commands=(cmds)
20
+ self.class.queued_commands[database_instance_key] = cmds
21
+ end
22
+
23
+ def in_multi
24
+ self.class.in_multi[database_instance_key]
25
+ end
26
+
27
+ def in_multi=(multi_state)
28
+ self.class.in_multi[database_instance_key] = multi_state
29
+ end
30
+ end
31
+ end
32
+
33
+ def discard
34
+ unless in_multi
35
+ raise Redis::CommandError, "ERR DISCARD without MULTI"
36
+ end
37
+
38
+ self.in_multi = false
39
+ self.queued_commands = []
40
+
41
+ 'OK'
42
+ end
43
+
44
+ def exec
45
+ unless in_multi
46
+ raise Redis::CommandError, "ERR EXEC without MULTI"
47
+ end
48
+
49
+ responses = queued_commands.map do |cmd|
50
+ begin
51
+ send(*cmd)
52
+ rescue => e
53
+ e
54
+ end
55
+ end
56
+
57
+ self.queued_commands = [] # reset queued_commands
58
+ self.in_multi = false # reset in_multi state
59
+
60
+ responses
61
+ end
62
+
63
+ def multi
64
+ if in_multi
65
+ raise Redis::CommandError, "ERR MULTI calls can not be nested"
66
+ end
67
+
68
+ self.in_multi = true
69
+
70
+ yield(self) if block_given?
71
+
72
+ "OK"
73
+ end
74
+
75
+ def watch(_)
76
+ "OK"
77
+ end
78
+
79
+ def unwatch
80
+ "OK"
81
+ end
82
+ end
83
+ end
@@ -1,3 +1,3 @@
1
1
  module FakeRedis
2
- VERSION = "0.4.3"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -11,15 +11,19 @@ module FakeRedis
11
11
  end
12
12
 
13
13
  def select_by_score min, max
14
- min = _floatify(min)
15
- max = _floatify(max)
14
+ min = _floatify(min, true)
15
+ max = _floatify(max, false)
16
16
  reject {|_,v| v < min || v > max }
17
17
  end
18
18
 
19
+ private
20
+
19
21
  # Originally lifted from redis-rb
20
- def _floatify(str)
22
+ def _floatify(str, increment = true)
21
23
  if (( inf = str.to_s.match(/^([+-])?inf/i) ))
22
24
  (inf[1] == "-" ? -1.0 : 1.0) / 0.0
25
+ elsif (( number = str.to_s.match(/^\((\d+)/i) ))
26
+ number[1].to_i + (increment ? 1 : -1)
23
27
  else
24
28
  Float str
25
29
  end
@@ -1,9 +1,12 @@
1
1
  require 'set'
2
2
  require 'redis/connection/registry'
3
3
  require 'redis/connection/command_helper'
4
+ require "fakeredis/command_executor"
4
5
  require "fakeredis/expiring_hash"
6
+ require "fakeredis/sort_method"
5
7
  require "fakeredis/sorted_set_argument_handler"
6
8
  require "fakeredis/sorted_set_store"
9
+ require "fakeredis/transaction_commands"
7
10
  require "fakeredis/zset"
8
11
 
9
12
  class Redis
@@ -11,8 +14,11 @@ class Redis
11
14
  class Memory
12
15
  include Redis::Connection::CommandHelper
13
16
  include FakeRedis
17
+ include SortMethod
18
+ include TransactionCommands
19
+ include CommandExecutor
14
20
 
15
- attr_accessor :buffer, :options
21
+ attr_accessor :options
16
22
 
17
23
  # Tracks all databases for all instances across the current process.
18
24
  # We have to be able to handle two clients with the same host/port accessing
@@ -76,25 +82,6 @@ class Redis
76
82
  def timeout=(usecs)
77
83
  end
78
84
 
79
- def write(command)
80
- meffod = command.shift.to_s.downcase.to_sym
81
- if respond_to?(meffod)
82
- reply = send(meffod, *command)
83
- else
84
- raise Redis::CommandError, "ERR unknown command '#{meffod}'"
85
- end
86
-
87
- if reply == true
88
- reply = 1
89
- elsif reply == false
90
- reply = 0
91
- end
92
-
93
- replies << reply
94
- buffer << reply if buffer && meffod != :multi
95
- nil
96
- end
97
-
98
85
  def read
99
86
  replies.shift
100
87
  end
@@ -103,8 +90,6 @@ class Redis
103
90
  # * blpop
104
91
  # * brpop
105
92
  # * brpoplpush
106
- # * discard
107
- # * sort
108
93
  # * subscribe
109
94
  # * psubscribe
110
95
  # * publish
@@ -150,7 +135,7 @@ class Redis
150
135
 
151
136
  def bgsave ; end
152
137
 
153
- def bgreriteaof ; end
138
+ def bgrewriteaof ; end
154
139
 
155
140
  def move key, destination_id
156
141
  raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
@@ -171,6 +156,11 @@ class Redis
171
156
  data[key].unpack('B*')[0].split("")[offset].to_i
172
157
  end
173
158
 
159
+ def bitcount(key, start_index = 0, end_index = -1)
160
+ return 0 unless data[key]
161
+ data[key][start_index..end_index].unpack('B*')[0].count("1")
162
+ end
163
+
174
164
  def getrange(key, start, ending)
175
165
  return unless data[key]
176
166
  data[key][start..ending]
@@ -214,8 +204,9 @@ class Redis
214
204
  def hdel(key, field)
215
205
  field = field.to_s
216
206
  data_type_check(key, Hash)
217
- data[key] && data[key].delete(field)
207
+ deleted = data[key] && data[key].delete(field)
218
208
  remove_key_for_empty_collection(key)
209
+ deleted ? 1 : 0
219
210
  end
220
211
 
221
212
  def hkeys(key)
@@ -244,6 +235,11 @@ class Redis
244
235
  Time.now.to_i
245
236
  end
246
237
 
238
+ def time
239
+ microseconds = (Time.now.to_f * 1000000).to_i
240
+ [ microseconds / 1000000, microseconds % 1000000 ]
241
+ end
242
+
247
243
  def dbsize
248
244
  data.keys.count
249
245
  end
@@ -267,17 +263,17 @@ class Redis
267
263
  data_type_check(key, Array)
268
264
  return unless data[key]
269
265
 
270
- if start < 0 && data[key].count < start.abs
271
- # Example: we have a list of 3 elements and
272
- # we give it a ltrim list, -5, -1. This means
273
- # it should trim to a max of 5. Since 3 < 5
274
- # we should not touch the list. This is consistent
275
- # with behavior of real Redis's ltrim with a negative
276
- # start argument.
277
- data[key]
278
- else
266
+ # Example: we have a list of 3 elements and
267
+ # we give it a ltrim list, -5, -1. This means
268
+ # it should trim to a max of 5. Since 3 < 5
269
+ # we should not touch the list. This is consistent
270
+ # with behavior of real Redis's ltrim with a negative
271
+ # start argument.
272
+ unless start < 0 && data[key].count < start.abs
279
273
  data[key] = data[key][start..stop]
280
274
  end
275
+
276
+ "OK"
281
277
  end
282
278
 
283
279
  def lindex(key, index)
@@ -360,7 +356,7 @@ class Redis
360
356
  def rpoplpush(key1, key2)
361
357
  data_type_check(key1, Array)
362
358
  rpop(key1).tap do |elem|
363
- lpush(key2, elem)
359
+ lpush(key2, elem) unless elem.nil?
364
360
  end
365
361
  end
366
362
 
@@ -403,7 +399,17 @@ class Redis
403
399
 
404
400
  def srem(key, value)
405
401
  data_type_check(key, ::Set)
406
- deleted = !!(data[key] && data[key].delete?(value.to_s))
402
+ return false unless data[key]
403
+
404
+ if value.is_a?(Array)
405
+ old_size = data[key].size
406
+ values = value.map(&:to_s)
407
+ values.each { |value| data[key].delete(value) }
408
+ deleted = old_size - data[key].size
409
+ else
410
+ deleted = !!data[key].delete?(value.to_s)
411
+ end
412
+
407
413
  remove_key_for_empty_collection(key)
408
414
  deleted
409
415
  end
@@ -473,10 +479,8 @@ class Redis
473
479
  data[destination] = ::Set.new(result)
474
480
  end
475
481
 
476
- def srandmember(key)
477
- data_type_check(key, ::Set)
478
- return nil unless data[key]
479
- data[key].to_a[rand(data[key].size)]
482
+ def srandmember(key, number=nil)
483
+ number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
480
484
  end
481
485
 
482
486
  def del(*keys)
@@ -516,9 +520,9 @@ class Redis
516
520
  end
517
521
 
518
522
  def expire(key, ttl)
519
- return unless data[key]
523
+ return 0 unless data[key]
520
524
  data.expires[key] = Time.now + ttl
521
- true
525
+ 1
522
526
  end
523
527
 
524
528
  def ttl(key)
@@ -586,7 +590,7 @@ class Redis
586
590
  raise_argument_error('hmget') if fields.empty?
587
591
 
588
592
  data_type_check(key, Hash)
589
- fields.map do |field|
593
+ fields.flatten.map do |field|
590
594
  field = field.to_s
591
595
  if data[key]
592
596
  data[key][field]
@@ -619,6 +623,17 @@ class Redis
619
623
  data[key][field].to_i
620
624
  end
621
625
 
626
+ def hincrbyfloat(key, field, increment)
627
+ data_type_check(key, Hash)
628
+ field = field.to_s
629
+ if data[key]
630
+ data[key][field] = (data[key][field].to_f + increment.to_f).to_s
631
+ else
632
+ data[key] = { field => increment.to_s }
633
+ end
634
+ data[key][field]
635
+ end
636
+
622
637
  def hexists(key, field)
623
638
  data_type_check(key, Hash)
624
639
  return false unless data[key]
@@ -635,8 +650,23 @@ class Redis
635
650
  set(key, value)
636
651
  end
637
652
 
638
- def set(key, value)
653
+ def set(key, value, *array_options)
654
+ option_nx = array_options.delete("NX")
655
+ option_xx = array_options.delete("XX")
656
+
657
+ return false if option_nx && option_xx
658
+
659
+ return false if option_nx && exists(key)
660
+ return false if option_xx && !exists(key)
661
+
639
662
  data[key] = value.to_s
663
+
664
+ options = Hash[array_options.each_slice(2).to_a]
665
+ ttl_in_seconds = options["EX"] if options["EX"]
666
+ ttl_in_seconds = options["PX"] / 1000.0 if options["PX"]
667
+
668
+ expire(key, ttl_in_seconds) if ttl_in_seconds
669
+
640
670
  "OK"
641
671
  end
642
672
 
@@ -644,7 +674,7 @@ class Redis
644
674
  old_val = data[key] ? data[key].unpack('B*')[0].split("") : []
645
675
  size_increment = [((offset/8)+1)*8-old_val.length, 0].max
646
676
  old_val += Array.new(size_increment).map{"0"}
647
- original_val = old_val[offset]
677
+ original_val = old_val[offset].to_i
648
678
  old_val[offset] = bit.to_s
649
679
  new_val = ""
650
680
  old_val.each_slice(8){|b| new_val = new_val + b.join("").to_i(2).chr }
@@ -687,10 +717,6 @@ class Redis
687
717
  true
688
718
  end
689
719
 
690
- def sort(key)
691
- # TODO: Implement
692
- end
693
-
694
720
  def incr(key)
695
721
  data.merge!({ key => (data[key].to_i + 1).to_s || "1"})
696
722
  data[key].to_i
@@ -728,22 +754,37 @@ class Redis
728
754
 
729
755
  def slaveof(host, port) ; end
730
756
 
731
- def exec
732
- buffer.tap {|x| self.buffer = nil }
733
- end
757
+ def scan(start_cursor, *args)
758
+ match = "*"
759
+ count = 10
734
760
 
735
- def multi
736
- self.buffer = []
737
- yield if block_given?
738
- "OK"
739
- end
761
+ if args.size.odd?
762
+ raise_argument_error('scan')
763
+ end
740
764
 
741
- def watch(_)
742
- "OK"
743
- end
765
+ if idx = args.index("MATCH")
766
+ match = args[idx + 1]
767
+ end
744
768
 
745
- def unwatch
746
- "OK"
769
+ if idx = args.index("COUNT")
770
+ count = args[idx + 1]
771
+ end
772
+
773
+ start_cursor = start_cursor.to_i
774
+ data_type_check(start_cursor, Fixnum)
775
+
776
+ cursor = start_cursor
777
+ next_keys = []
778
+
779
+ if start_cursor + count >= data.length
780
+ next_keys = keys(match)[start_cursor..-1]
781
+ cursor = 0
782
+ else
783
+ cursor = start_cursor + 10
784
+ next_keys = keys(match)[start_cursor..cursor]
785
+ end
786
+
787
+ return "#{cursor}", next_keys
747
788
  end
748
789
 
749
790
  def zadd(key, *args)
@@ -900,6 +941,17 @@ class Redis
900
941
  range.size
901
942
  end
902
943
 
944
+ def zremrangebyrank(key, start, stop)
945
+ data_type_check(key, ZSet)
946
+ return 0 unless data[key]
947
+
948
+ sorted_elements = data[key].sort_by { |k, v| v }
949
+ start = sorted_elements.length if start > sorted_elements.length
950
+ elements_to_delete = sorted_elements[start..stop]
951
+ elements_to_delete.each { |elem, rank| data[key].delete(elem) }
952
+ elements_to_delete.size
953
+ end
954
+
903
955
  def zinterstore(out, *args)
904
956
  data_type_check(out, ZSet)
905
957
  args_handler = SortedSetArgumentHandler.new(args)
@@ -914,14 +966,6 @@ class Redis
914
966
  data[out].size
915
967
  end
916
968
 
917
- def zremrangebyrank(key, start, stop)
918
- sorted_elements = data[key].sort_by { |k, v| v }
919
- start = sorted_elements.length if start > sorted_elements.length
920
- elements_to_delete = sorted_elements[start..stop]
921
- elements_to_delete.each { |elem, rank| data[key].delete(elem) }
922
- elements_to_delete.size
923
- end
924
-
925
969
  private
926
970
  def raise_argument_error(command, match_string=command)
927
971
  error_message = if %w(hmset mset_odd).include?(match_string.downcase)
@@ -964,6 +1008,25 @@ class Redis
964
1008
  def mapped_param? param
965
1009
  param.size == 1 && param[0].is_a?(Array)
966
1010
  end
1011
+
1012
+ def srandmember_single(key)
1013
+ data_type_check(key, ::Set)
1014
+ return nil unless data[key]
1015
+ data[key].to_a[rand(data[key].size)]
1016
+ end
1017
+
1018
+ def srandmember_multiple(key, number)
1019
+ return [] unless data[key]
1020
+ if number >= 0
1021
+ # replace with `data[key].to_a.sample(number)` when 1.8.7 is deprecated
1022
+ (1..number).inject([]) do |selected, _|
1023
+ available_elements = data[key].to_a - selected
1024
+ selected << available_elements[rand(available_elements.size)]
1025
+ end.compact
1026
+ else
1027
+ (1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
1028
+ end
1029
+ end
967
1030
  end
968
1031
  end
969
1032
  end