fakeredis 0.4.3 → 0.5.0

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: 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