cell_set 0.1.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.
- data/README +5 -0
- data/Rakefile +1 -0
- data/lib/cell_set/attributes.rb +62 -0
- data/lib/cell_set/cell.rb +24 -0
- data/lib/cell_set/cell_set.rb +152 -0
- data/lib/cell_set/cell_set_axis.rb +79 -0
- data/lib/cell_set/member.rb +75 -0
- data/lib/cell_set/model.rb +5 -0
- data/lib/cell_set/position.rb +60 -0
- data/lib/cell_set/result.rb +9 -0
- data/lib/cell_set/version.rb +3 -0
- data/lib/cell_set.rb +25 -0
- data/lib/mondrian/olap/cell_set/ruby.rb +61 -0
- data/spec/factories/cell_set_axis_factory.rb +4 -0
- data/spec/factories/cell_set_factory.rb +3 -0
- data/spec/factories/member_factory.rb +5 -0
- data/spec/factories/position_factory.rb +4 -0
- data/spec/factories/sequences.rb +7 -0
- data/spec/fixtures/FoodMart.xml +778 -0
- data/spec/fixtures/database.yml +19 -0
- data/spec/fixtures/postgresql-9.0-801.jdbc4.jar +0 -0
- data/spec/models/cell_set_axis_spec.rb +86 -0
- data/spec/models/cell_set_spec.rb +160 -0
- data/spec/models/member_spec.rb +68 -0
- data/spec/models/mondrian/olap/mondrian_olap4j_cell_set_spec.rb +109 -0
- data/spec/models/mondrian/olap/result_spec.rb +17 -0
- data/spec/models/position_spec.rb +83 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/matchers/have_accessor.rb +7 -0
- metadata +166 -0
data/README
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
cell_set is a ruby implementation of the CellSet and related models from mondrian.olap4j.
|
2
|
+
|
3
|
+
The cell_set gem automatically mixes in a to_ruby method on Mondrian::OLAP::CellSet that can be called from the instance variable @raw_cell_set in an instance of Mondrian::OLAP::Result.
|
4
|
+
|
5
|
+
Normally the to_ruby method is automatically called in the to_cell_set method in an instance of Mondrian::OLAP::Result.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Use this module to add necessary attributes methods that ActiveModel::AttributeMethods doesn't add
|
2
|
+
# An ATTRIBUTES constant must be on the model before including this module
|
3
|
+
# Attributes specified in ATTRIBUTES will be serialized if any ActiveModel::Serializers are used
|
4
|
+
# Any attributes that shouldn't be serialized can still be set with another attr_accessor, but won't show up in
|
5
|
+
#
|
6
|
+
|
7
|
+
module CellSet
|
8
|
+
module Attributes
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
extend ActiveModel::Naming
|
11
|
+
include ActiveModel::AttributeMethods
|
12
|
+
|
13
|
+
included do
|
14
|
+
self.send(:attr_accessor, *self::ATTRIBUTES)
|
15
|
+
|
16
|
+
if self.private_methods.include?(:define_attribute_method)
|
17
|
+
self.send(:define_attribute_method, self::ATTRIBUTES)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def human_attribute_name(attr, options = {})
|
23
|
+
attr
|
24
|
+
end
|
25
|
+
|
26
|
+
def lookup_ancestors
|
27
|
+
[self]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(attrs = {})
|
32
|
+
@errors = ActiveModel::Errors.new(self)
|
33
|
+
attrs.each do |name, value|
|
34
|
+
send("#{name}=", value) if value
|
35
|
+
end
|
36
|
+
super(attrs)
|
37
|
+
end
|
38
|
+
|
39
|
+
def attributes
|
40
|
+
self.class::ATTRIBUTES.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, key|
|
41
|
+
result[key] = read_attribute_for_validation(key)
|
42
|
+
result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def attributes=(attrs)
|
47
|
+
attrs.each_pair {|k, v| send("#{k}=", v)}
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear_attribute(attr)
|
51
|
+
send("#{attr}=", nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_attribute?(attr)
|
55
|
+
self.class::ATTRIBUTES.include?(attr)
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_attribute_for_validation(key)
|
59
|
+
send(key)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CellSet
|
2
|
+
class Cell
|
3
|
+
ATTRIBUTES = [:formatted_value, :ordinal, :value]
|
4
|
+
|
5
|
+
include Attributes
|
6
|
+
include ActiveModel::Serializers::JSON
|
7
|
+
include ActiveModel::Serializers::Xml
|
8
|
+
self.include_root_in_json = false
|
9
|
+
|
10
|
+
def from_json(*)
|
11
|
+
super.tap{|obj| obj.freeze}
|
12
|
+
end
|
13
|
+
|
14
|
+
def ordinal=(ordinal)
|
15
|
+
@ordinal = if ordinal.is_a?(Fixnum)
|
16
|
+
ordinal
|
17
|
+
elsif ordinal.is_a?(String)
|
18
|
+
Integer(ordinal)
|
19
|
+
else
|
20
|
+
throw ArgumentError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'cell_set/cell_set_axis'
|
2
|
+
require 'cell_set/cell'
|
3
|
+
|
4
|
+
module CellSet
|
5
|
+
class CellSet
|
6
|
+
ATTRIBUTES = [:axes]
|
7
|
+
|
8
|
+
include Attributes
|
9
|
+
include ActiveModel::Serializers::JSON
|
10
|
+
include ActiveModel::Serializers::Xml
|
11
|
+
|
12
|
+
def as_json(options = nil)
|
13
|
+
# Strangely couldn't use options = {} in JRuby
|
14
|
+
options = {} if options.nil?
|
15
|
+
json = super(options)
|
16
|
+
unless (options.has_key?(:only) && !options[:only].include?("cells")) ||
|
17
|
+
(options.has_key?(:except) && options[:except].include?("cells"))
|
18
|
+
json.first[1]["cells"] = @cellHash
|
19
|
+
end
|
20
|
+
json
|
21
|
+
end
|
22
|
+
|
23
|
+
def axes=(axes)
|
24
|
+
if axes.is_a?(Array)
|
25
|
+
@axes = axes.map do |axis|
|
26
|
+
if axis.is_a?(CellSetAxis)
|
27
|
+
axis.cell_set = self
|
28
|
+
axis
|
29
|
+
elsif axis.is_a?(Hash)
|
30
|
+
CellSetAxis.new(:cell_set => self).from_json(axis.to_json)
|
31
|
+
else
|
32
|
+
throw ArgumentError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
else
|
36
|
+
throw ArgumentError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def bounds
|
41
|
+
@axes.map{|axis| axis.positionCount}
|
42
|
+
end
|
43
|
+
|
44
|
+
def cell(*args)
|
45
|
+
if args[0].is_a?(NilClass)
|
46
|
+
throw(ArgumentError)
|
47
|
+
elsif args[0].is_a?(Fixnum) && args.length == 1
|
48
|
+
getCellInternal(args[0])
|
49
|
+
elsif args[0].is_a?(Array) && args.length == 1
|
50
|
+
getCellInternal(coordinatesToOrdinal(args[0]))
|
51
|
+
elsif args.select{|arg| arg.is_a?(Position)}.length == args.length
|
52
|
+
unless args.length == axes.length
|
53
|
+
throw ArgumentError, "cell coordinates should have dimension #{axis.length}"
|
54
|
+
end
|
55
|
+
cell(args.map{|position| position.getOrdinal})
|
56
|
+
else
|
57
|
+
throw(ArgumentError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def cells=(cells)
|
62
|
+
if cells.is_a?(Array)
|
63
|
+
cells.each do |cell|
|
64
|
+
if cell.is_a?(Cell)
|
65
|
+
@cellHash[cell.ordinal.to_s] = {
|
66
|
+
"formatted_value" => cell.formatted_value,
|
67
|
+
"value" => cell.value
|
68
|
+
}
|
69
|
+
else
|
70
|
+
throw ArgumentError
|
71
|
+
end
|
72
|
+
end
|
73
|
+
elsif cells.is_a?(Hash)
|
74
|
+
@cellHash = cells
|
75
|
+
else
|
76
|
+
throw ArgumentError
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def coordinatesToOrdinal(coordinates)
|
81
|
+
unless coordinates.length == @axes.length
|
82
|
+
throw ArgumentError, "Coordinates have different dimension #{coordinates.length} than axes #{@axes.length}"
|
83
|
+
end
|
84
|
+
|
85
|
+
modulo = 1
|
86
|
+
ordinal = 0
|
87
|
+
k = 0
|
88
|
+
@axes.each do |axis|
|
89
|
+
coordinate = coordinates[k]
|
90
|
+
if coordinate < 0 || coordinate >= axis.positionCount
|
91
|
+
throw IndexError, "Coordinate #{coordinate} of axis #{k} is out of range (#{bounds.join(", ")})"
|
92
|
+
end
|
93
|
+
ordinal += coordinate * modulo
|
94
|
+
modulo *= axis.positionCount
|
95
|
+
end
|
96
|
+
ordinal
|
97
|
+
end
|
98
|
+
|
99
|
+
def from_json(*)
|
100
|
+
super.tap{|obj| obj.freeze}
|
101
|
+
end
|
102
|
+
|
103
|
+
def getFilterAxis
|
104
|
+
throw NotImplementedError
|
105
|
+
end
|
106
|
+
|
107
|
+
def initialize(*)
|
108
|
+
@cellHash = {}
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
def maxOrdinal
|
113
|
+
modulo = 1
|
114
|
+
@axes.each do |axis|
|
115
|
+
modulo *= axis.positionCount
|
116
|
+
end
|
117
|
+
modulo
|
118
|
+
end
|
119
|
+
|
120
|
+
def ordinalToCoordinates(ordinal)
|
121
|
+
modulo = 1
|
122
|
+
|
123
|
+
list = @axes.map do |axis|
|
124
|
+
prevModulo = modulo
|
125
|
+
modulo *= axis.positionCount
|
126
|
+
(ordinal % modulo) / prevModulo
|
127
|
+
end
|
128
|
+
|
129
|
+
if ordinal < 0 || ordinal >= modulo
|
130
|
+
throw IndexError, "Cell ordinal #{ordinal} lies outside CellSet bounds (#{bounds.join(", ")})"
|
131
|
+
end
|
132
|
+
|
133
|
+
list
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
attr_accessor :cellHash
|
138
|
+
|
139
|
+
def getCellInternal(pos)
|
140
|
+
cell = @cellHash[pos.to_s]
|
141
|
+
if cell.nil?
|
142
|
+
if (pos < 0 || pos >= maxOrdinal)
|
143
|
+
throw IndexError
|
144
|
+
else
|
145
|
+
Cell.new(:formatted_value => "", :ordinal => pos)
|
146
|
+
end
|
147
|
+
else
|
148
|
+
Cell.new(:formatted_value => cell["formatted_value"], :ordinal => pos, :value => cell["value"])
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'cell_set/position'
|
2
|
+
|
3
|
+
module CellSet
|
4
|
+
class CellSetAxis
|
5
|
+
ATTRIBUTES = [:axis_ordinal, :cell_set, :positions]
|
6
|
+
|
7
|
+
include Attributes
|
8
|
+
include ActiveModel::Serializers::JSON
|
9
|
+
include ActiveModel::Serializers::Xml
|
10
|
+
include Comparable
|
11
|
+
self.include_root_in_json = false
|
12
|
+
|
13
|
+
def <=>(other)
|
14
|
+
if other.is_a?(self.class)
|
15
|
+
@axis_ordinal <=> other.axis_ordinal
|
16
|
+
else
|
17
|
+
raise(ArgumentError, "Must compare #{self.class.to_s} to another #{self.class.to_s}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def axis_ordinal=(ordinal)
|
22
|
+
@axis_ordinal = if ordinal.is_a?(Fixnum)
|
23
|
+
ordinal
|
24
|
+
elsif ordinal.is_a?(String)
|
25
|
+
Integer(ordinal)
|
26
|
+
else
|
27
|
+
throw ArgumentError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def between?(min, max)
|
32
|
+
@axis_ordinal <= min || @axis_ordinal >= max
|
33
|
+
end
|
34
|
+
|
35
|
+
def cell_set=(cell_set)
|
36
|
+
@cell_set = if cell_set.is_a?(CellSet)
|
37
|
+
cell_set
|
38
|
+
elsif cell_set.is_a?(Hash)
|
39
|
+
CellSet.new.from_json(cell_set.to_json)
|
40
|
+
else
|
41
|
+
throw ArgumentError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def from_json(*)
|
46
|
+
super.tap{|obj| obj.freeze}
|
47
|
+
end
|
48
|
+
|
49
|
+
def positionCount
|
50
|
+
@positions.length
|
51
|
+
end
|
52
|
+
|
53
|
+
def positions=(positions)
|
54
|
+
if positions.is_a?(Array)
|
55
|
+
@positions = positions.map do |position|
|
56
|
+
if position.is_a?(Position)
|
57
|
+
position
|
58
|
+
elsif position.is_a?(Hash)
|
59
|
+
Position.new.from_json(position.to_json)
|
60
|
+
else
|
61
|
+
throw ArgumentError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
else
|
65
|
+
throw ArgumentError
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def as_json(options = nil)
|
70
|
+
# Strangely couldn't use options = {} in JRuby
|
71
|
+
options = {} if options.nil?
|
72
|
+
unless (options.has_key?(:only) && options[:only].include?("cell_set")) ||
|
73
|
+
(options.has_key?(:except) && options[:except].include?("cell_set"))
|
74
|
+
options[:except] = (options[:except] || []).push("cell_set")
|
75
|
+
end
|
76
|
+
super(options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module CellSet
|
2
|
+
class Member
|
3
|
+
ATTRIBUTES = [:caption, :childMembers, :depth, :description, :dimension, :expression, :hierarchy, :level, :memberType, \
|
4
|
+
:name, :ordinal, :parentMember, :uniqueName]
|
5
|
+
|
6
|
+
include Attributes
|
7
|
+
include ActiveModel::Serializers::JSON
|
8
|
+
include ActiveModel::Serializers::Xml
|
9
|
+
include Comparable
|
10
|
+
self.include_root_in_json = false
|
11
|
+
|
12
|
+
def <=>(other)
|
13
|
+
if other.is_a?(self.class)
|
14
|
+
@ordinal <=> other.ordinal
|
15
|
+
else
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def ancestorMembers
|
21
|
+
throw NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def between?(min, max)
|
25
|
+
@ordinal <= min || @ordinal >= max
|
26
|
+
end
|
27
|
+
|
28
|
+
def childMemberCount
|
29
|
+
throw NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def all?
|
33
|
+
throw NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def calculated?
|
37
|
+
throw NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def calculatedInQuery?
|
41
|
+
throw NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def childOrEqualTo?(member)
|
45
|
+
throw NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def from_json(*)
|
49
|
+
super.tap{|obj| obj.freeze}
|
50
|
+
end
|
51
|
+
|
52
|
+
def hidden?
|
53
|
+
throw NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(*)
|
57
|
+
@childMembers = []
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def ordinal=(ordinal)
|
62
|
+
@ordinal = if ordinal.is_a?(Fixnum)
|
63
|
+
ordinal
|
64
|
+
elsif ordinal.is_a?(String)
|
65
|
+
Integer(ordinal)
|
66
|
+
else
|
67
|
+
throw ArgumentError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def visible?
|
72
|
+
throw NotImplementedError
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'cell_set/member'
|
2
|
+
|
3
|
+
module CellSet
|
4
|
+
class Position
|
5
|
+
ATTRIBUTES = [:members, :ordinal]
|
6
|
+
|
7
|
+
include Attributes
|
8
|
+
include ActiveModel::Serializers::JSON
|
9
|
+
include ActiveModel::Serializers::Xml
|
10
|
+
include Comparable
|
11
|
+
self.include_root_in_json = false
|
12
|
+
|
13
|
+
def <=>(other)
|
14
|
+
if other.is_a?(self.class)
|
15
|
+
@ordinal <=> other.ordinal
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def between?(min, max)
|
22
|
+
@ordinal <= min || @ordinal >= max
|
23
|
+
end
|
24
|
+
|
25
|
+
def from_json(*)
|
26
|
+
super.tap{|obj| obj.freeze}
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(*)
|
30
|
+
@members = []
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def members=(members)
|
35
|
+
if members.is_a?(Array)
|
36
|
+
@members = members.map do |member|
|
37
|
+
if member.is_a?(Member)
|
38
|
+
member
|
39
|
+
elsif member.is_a?(Hash)
|
40
|
+
Member.new.from_json(member.to_json)
|
41
|
+
else
|
42
|
+
throw ArgumentError
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
throw ArgumentError
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def ordinal=(ordinal)
|
51
|
+
@ordinal = if ordinal.is_a?(Fixnum)
|
52
|
+
ordinal
|
53
|
+
elsif ordinal.is_a?(String)
|
54
|
+
Integer(ordinal)
|
55
|
+
else
|
56
|
+
throw ArgumentError
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/cell_set.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/hash_with_indifferent_access'
|
3
|
+
require 'active_model/attribute_methods'
|
4
|
+
require 'active_model/naming'
|
5
|
+
require 'active_model/serialization'
|
6
|
+
require 'active_model/serializers/json'
|
7
|
+
require 'active_model/serializers/xml'
|
8
|
+
if RUBY_PLATFORM == "java"
|
9
|
+
require 'mondrian-olap'
|
10
|
+
end
|
11
|
+
|
12
|
+
%w(version attributes result cell member position cell_set_axis cell_set model).each do |file|
|
13
|
+
require "cell_set/#{file}"
|
14
|
+
end
|
15
|
+
|
16
|
+
%w(ruby).each do |file|
|
17
|
+
require "mondrian/olap/cell_set/#{file}"
|
18
|
+
end
|
19
|
+
|
20
|
+
if RUBY_PLATFORM == "java"
|
21
|
+
ActiveSupport.on_load(:cell_set_model) do
|
22
|
+
Java::Mondrian::olap4j::MondrianOlap4jCellSet.send(:include, Mondrian::OLAP::CellSet::Ruby)
|
23
|
+
Mondrian::OLAP::Result.send(:include, ::CellSet::Result)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Mondrian
|
2
|
+
module OLAP
|
3
|
+
module CellSet
|
4
|
+
module Ruby
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def to_ruby
|
8
|
+
cell_set = ::CellSet::CellSet.new
|
9
|
+
axes = getAxes.map do |axis|
|
10
|
+
::CellSet::CellSetAxis.new(:axis_ordinal => axis.getAxisOrdinal.axisOrdinal, :cell_set => cell_set, :positions => buildPositions(axis))
|
11
|
+
end
|
12
|
+
cell_set.axes = axes
|
13
|
+
cell_set.send("cellHash=", getCellHash)
|
14
|
+
cell_set.freeze
|
15
|
+
cell_set
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def buildPositions(axis)
|
20
|
+
axis.positions.map do |position|
|
21
|
+
members = position.getMembers.map do |member|
|
22
|
+
::CellSet::Member.new(
|
23
|
+
:caption => member.getCaption,
|
24
|
+
:depth => member.getDepth,
|
25
|
+
:description => member.getDescription,
|
26
|
+
:memberType => member.getMemberType.to_s,
|
27
|
+
:name => member.getName,
|
28
|
+
:ordinal => member.getOrdinal,
|
29
|
+
:uniqueName => member.getUniqueName
|
30
|
+
)
|
31
|
+
end
|
32
|
+
::CellSet::Position.new(:members => members, :ordinal => position.getOrdinal)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def getCellHash
|
37
|
+
cells = {}
|
38
|
+
maxOrdinal.times do |i|
|
39
|
+
formatted_value, value = *getCell(i).try{|cell| [cell.getFormattedValue, cell.getValue]}
|
40
|
+
unless formatted_value.blank? && value.nil?
|
41
|
+
# Build key as string
|
42
|
+
cells[i.to_s] = {
|
43
|
+
"formatted_value" => formatted_value,
|
44
|
+
"value" => value
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
cells
|
49
|
+
end
|
50
|
+
|
51
|
+
def maxOrdinal
|
52
|
+
modulo = 1
|
53
|
+
getAxes.each do |axis|
|
54
|
+
modulo *= axis.getPositionCount
|
55
|
+
end
|
56
|
+
modulo
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|