MINT-statemachine 1.2.3 → 1.3.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/CHANGES +22 -0
- data/LICENSE +2 -2
- data/MINT-statemachine.gemspec +12 -12
- data/README.rdoc +1 -1
- data/lib/statemachine/action_invokation.rb +51 -4
- data/lib/statemachine/builder.rb +18 -1
- data/lib/statemachine/generate/dot_graph/dot_graph_statemachine.rb +2 -2
- data/lib/statemachine/generate/java/java_statemachine.rb +4 -4
- data/lib/statemachine/parallelstate.rb +97 -49
- data/lib/statemachine/state.rb +54 -13
- data/lib/statemachine/statemachine.rb +53 -18
- data/lib/statemachine/superstate.rb +5 -1
- data/lib/statemachine/transition.rb +45 -4
- data/lib/statemachine/version.rb +2 -2
- data/spec/action_invokation_spec.rb +1 -1
- data/spec/builder_spec.rb +1 -1
- data/spec/default_transition_spec.rb +1 -1
- data/spec/generate/dot_graph/dot_graph_stagemachine_spec.rb +1 -1
- data/spec/history_spec.rb +1 -1
- data/spec/sm_action_parameterization_spec.rb +1 -1
- data/spec/sm_activation_spec.rb +31 -22
- data/spec/sm_entry_exit_actions_spec.rb +1 -1
- data/spec/sm_odds_n_ends_spec.rb +1 -1
- data/spec/sm_parallel_state_spec.rb +48 -8
- data/spec/sm_simple_spec.rb +1 -1
- data/spec/sm_super_state_spec.rb +1 -1
- data/spec/sm_turnstile_spec.rb +5 -5
- data/spec/spec_helper.rb +7 -5
- data/spec/transition_spec.rb +103 -7
- metadata +58 -75
data/CHANGES
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
= Statemachine Changelog
|
2
2
|
|
3
|
+
== Version 1.3.0
|
4
|
+
|
5
|
+
* (Sebastian) fixed bug that prevented exit call on parallel state exit
|
6
|
+
* (Sebastian) refactorings to support existing parallel states without calling events twice
|
7
|
+
* (Sebastian) if entering a parallel state, spontaneous initial transitions were not executed
|
8
|
+
* (Sebastian) introduced workaround to retrieve correct activation callback within parallel state machines
|
9
|
+
* (Jessica) fixed bug that prevented spontaneous transitions from initial state within superstate
|
10
|
+
* (Jessica) added test using transition to self to recheck spontaneous transition's condition.
|
11
|
+
* (Jessica) transitions now stored as array
|
12
|
+
* (Jessica) correct abstract states handling working. All tests working
|
13
|
+
* (Jessica) fixed bug that prevented in parallel state machine setups the correct publishing of all new atomic and abstract states
|
14
|
+
* (Sebastian) added option to temporarily disable activation callback for reset
|
15
|
+
* (Sebastian) several minor bug fixes
|
16
|
+
* (Sebastian) changed activation callback processing so it can be used fo publishing state updates with redis
|
17
|
+
* (Jessica) Added tests for on_entry, on_exit and transitions for parallel states.
|
18
|
+
* (Sebastian) fixed optional parameter handling
|
19
|
+
* (Sebastian) allow dynamic parameter size
|
20
|
+
* (Jessica) No more In() hack. Use is_in? instead
|
21
|
+
* (Jessica) Support for spontaneous transitions
|
22
|
+
* (Sebastian) abstract_states now also includes parallel state name
|
23
|
+
* (Jessica) added treatment for ifs
|
24
|
+
|
3
25
|
== Version 1.2.3
|
4
26
|
|
5
27
|
* Added Gemfile for bundler
|
data/LICENSE
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
Copyright (C) 2010
|
2
|
-
Copyright (C) 2006-2010
|
1
|
+
Copyright (C) 2010-2012 Sebastian Feuerstack, Jessica Colnago
|
2
|
+
Copyright (C) 2006-2010 Micah Martin
|
3
3
|
|
4
4
|
This library is free software; you can redistribute it and/or
|
5
5
|
modify it under the terms of the GNU Lesser General Public
|
data/MINT-statemachine.gemspec
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
|
-
s.name =
|
5
|
-
s.version = "1.
|
4
|
+
s.name = "MINT-statemachine"
|
5
|
+
s.version = "1.3.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
-
s.authors = [
|
9
|
-
s.date =
|
10
|
-
s.description =
|
11
|
-
s.email =
|
12
|
-
s.files = [
|
13
|
-
s.homepage =
|
14
|
-
s.require_paths = [
|
15
|
-
s.rubygems_version =
|
16
|
-
s.summary =
|
17
|
-
s.test_files = [
|
8
|
+
s.authors = ["Sebastian Feuerstack"]
|
9
|
+
s.date = "2012-11-20"
|
10
|
+
s.description = "The MINT Statemachine is a ruby library for building Finite State Machines, based on the Statemachine gem by Micah Martin."
|
11
|
+
s.email = "Sebastian@Feuerstack.org"
|
12
|
+
s.files = ["TODO", "Gemfile.lock", "LICENSE", "CHANGES", "Rakefile", "Gemfile", "README.rdoc", "MINT-statemachine.gemspec", "lib/statemachine.rb", "lib/statemachine/builder.rb", "lib/statemachine/statemachine.rb", "lib/statemachine/generate/util.rb", "lib/statemachine/generate/dot_graph/dot_graph_statemachine.rb", "lib/statemachine/generate/java/java_statemachine.rb", "lib/statemachine/generate/dot_graph.rb", "lib/statemachine/generate/src_builder.rb", "lib/statemachine/generate/java.rb", "lib/statemachine/transition.rb", "lib/statemachine/version.rb", "lib/statemachine/parallelstate.rb", "lib/statemachine/action_invokation.rb", "lib/statemachine/superstate.rb", "lib/statemachine/state.rb", "lib/statemachine/stub_context.rb", "spec/default_transition_spec.rb", "spec/spec_helper.rb", "spec/sm_super_state_spec.rb", "spec/sm_action_parameterization_spec.rb", "spec/generate/dot_graph/dot_graph_stagemachine_spec.rb", "spec/generate/java/java_statemachine_spec.rb", "spec/sm_entry_exit_actions_spec.rb", "spec/sm_simple_spec.rb", "spec/sm_parallel_state_spec.rb", "spec/transition_spec.rb", "spec/noodle.rb", "spec/sm_activation_spec.rb", "spec/builder_spec.rb", "spec/action_invokation_spec.rb", "spec/sm_turnstile_spec.rb", "spec/history_spec.rb", "spec/sm_odds_n_ends_spec.rb"]
|
13
|
+
s.homepage = "http://www.multi-access.de"
|
14
|
+
s.require_paths = ["lib"]
|
15
|
+
s.rubygems_version = "1.8.15"
|
16
|
+
s.summary = "MINT-Statemachine-1.3.0 - Statemachine Library for Ruby based on statemachine from http://slagyr.github.com/statemachine http://www.multi-access.de/open-source-software/third-party-software-extensions/"
|
17
|
+
s.test_files = ["spec/default_transition_spec.rb", "spec/sm_super_state_spec.rb", "spec/sm_action_parameterization_spec.rb", "spec/sm_entry_exit_actions_spec.rb", "spec/sm_simple_spec.rb", "spec/sm_parallel_state_spec.rb", "spec/transition_spec.rb", "spec/sm_activation_spec.rb", "spec/builder_spec.rb", "spec/action_invokation_spec.rb", "spec/sm_turnstile_spec.rb", "spec/history_spec.rb", "spec/sm_odds_n_ends_spec.rb"]
|
18
18
|
|
19
19
|
if s.respond_to? :specification_version then
|
20
20
|
s.specification_version = 3
|
data/README.rdoc
CHANGED
@@ -51,7 +51,7 @@ http://www.multi-access.de
|
|
51
51
|
|
52
52
|
== License
|
53
53
|
|
54
|
-
Copyright (C) 2010
|
54
|
+
Copyright (C) 2010-2012 Sebastian Feuerstack, Jessica Colnago
|
55
55
|
Copyright (C) 2006-2010 Micah Martin
|
56
56
|
|
57
57
|
This library is free software; you can redistribute it and/or
|
@@ -20,13 +20,40 @@ module Statemachine
|
|
20
20
|
result = send(a[1],a[2])
|
21
21
|
elsif a[0] == 'invoke'
|
22
22
|
result = invoke_method(a[1],args, message)
|
23
|
+
elsif a[0] == 'script'
|
24
|
+
result = invoke_string(a[1])
|
25
|
+
result = true if result == nil
|
26
|
+
elsif a[0] == "if"
|
27
|
+
result = invoke_string(a[1])
|
28
|
+
if result
|
29
|
+
result = invoke_action(a[2], [], message, messenger, message_queue)
|
30
|
+
return result
|
31
|
+
else
|
32
|
+
result = true
|
33
|
+
end
|
34
|
+
elsif a[0] == "elseif"
|
35
|
+
result = invoke_string(a[1])
|
36
|
+
if result
|
37
|
+
result = invoke_action(a[2], [], message, messenger, message_queue)
|
38
|
+
return result
|
39
|
+
else
|
40
|
+
result = true
|
41
|
+
end
|
42
|
+
elsif a[0] == "else"
|
43
|
+
result = a[1]
|
44
|
+
if result
|
45
|
+
result = invoke_action(a[2], [], message, messenger, message_queue)
|
46
|
+
return result
|
47
|
+
else
|
48
|
+
result = true
|
49
|
+
end
|
23
50
|
end
|
24
51
|
else
|
25
52
|
log("#{a}")
|
26
53
|
result = invoke_string(a) if not messenger
|
27
54
|
end
|
28
55
|
return false if result == false
|
29
|
-
|
56
|
+
}
|
30
57
|
result
|
31
58
|
end
|
32
59
|
|
@@ -66,13 +93,33 @@ module Statemachine
|
|
66
93
|
end
|
67
94
|
end
|
68
95
|
|
96
|
+
#def params_for_block(block, args, message)
|
97
|
+
# arity = block.arity
|
98
|
+
# required_params = arity < 0 ? arity.abs - 1 : arity
|
99
|
+
#
|
100
|
+
# raise StatemachineException.new("Insufficient parameters. (#{message})") if required_params > args.length
|
101
|
+
#
|
102
|
+
# arity < 0 ? args : args[0...arity]
|
103
|
+
#end
|
104
|
+
|
105
|
+
# extended parameter handling that allows to define procs with variable parameter size and also
|
106
|
+
# supports cutting the arguments to match a fixed set of parameters
|
107
|
+
|
69
108
|
def params_for_block(block, args, message)
|
70
|
-
|
71
|
-
|
109
|
+
i = block.parameters
|
110
|
+
|
111
|
+
required_params = block.parameters.select {|x| x[0].eql? :req or x[0].eql? :opt}.length
|
112
|
+
|
113
|
+
includes_optional = block.parameters.select {|x| x[0].eql? :rest}.length > 0
|
72
114
|
|
73
115
|
raise StatemachineException.new("Insufficient parameters. (#{message})") if required_params > args.length
|
74
116
|
|
75
|
-
|
117
|
+
# in case we have no optional parameters but more args than parameters, vut the args to match the parameters
|
118
|
+
if args.length>required_params and includes_optional == false
|
119
|
+
args[0...required_params]
|
120
|
+
else
|
121
|
+
args
|
122
|
+
end
|
76
123
|
end
|
77
124
|
|
78
125
|
end
|
data/lib/statemachine/builder.rb
CHANGED
@@ -265,7 +265,7 @@ module Statemachine
|
|
265
265
|
s = statemachine.get_state(id)
|
266
266
|
if (s)
|
267
267
|
statemachine.remove_state(@subject)
|
268
|
-
s.transitions.each {|
|
268
|
+
s.transitions.each {|v|
|
269
269
|
@subject.add(v)
|
270
270
|
}
|
271
271
|
end
|
@@ -344,6 +344,23 @@ module Statemachine
|
|
344
344
|
statemachine.add_state(@subject)
|
345
345
|
#puts "added #{@subject.inspect}"
|
346
346
|
end
|
347
|
+
|
348
|
+
def on_entry(entry_action)
|
349
|
+
@subject.entry_action = entry_action
|
350
|
+
end
|
351
|
+
|
352
|
+
def on_exit(exit_action)
|
353
|
+
@subject.exit_action = exit_action
|
354
|
+
end
|
355
|
+
|
356
|
+
def event(event, destination_id, action = nil, cond = true)
|
357
|
+
@subject.add(Transition.new(@subject.id, destination_id, event, action, cond))
|
358
|
+
end
|
359
|
+
|
360
|
+
def trans(origin_id, event, destination_id, action = nil, cond = true)
|
361
|
+
origin = acquire_state_in(origin_id, @subject)
|
362
|
+
origin.add(Transition.new(origin_id, destination_id, event, action, cond))
|
363
|
+
end
|
347
364
|
end
|
348
365
|
|
349
366
|
# Created by Statemachine.build as the root context for building the statemachine.
|
@@ -41,7 +41,7 @@ module Statemachine
|
|
41
41
|
@nodes = []
|
42
42
|
@transitions = []
|
43
43
|
@sm.states.values.each { |state|
|
44
|
-
state.transitions.
|
44
|
+
state.transitions.each { |transition|
|
45
45
|
@nodes << transition.origin_id
|
46
46
|
@nodes << transition.destination_id
|
47
47
|
@transitions << transition
|
@@ -72,7 +72,7 @@ module Statemachine
|
|
72
72
|
|
73
73
|
add_graph_header(builder, state.id)
|
74
74
|
|
75
|
-
state.transitions.
|
75
|
+
state.transitions.each do |transition|
|
76
76
|
add_transition(builder, transition)
|
77
77
|
end
|
78
78
|
|
@@ -46,7 +46,7 @@ module Statemachine
|
|
46
46
|
events = []
|
47
47
|
actions = []
|
48
48
|
@sm.states.values.each do |state|
|
49
|
-
state.transitions.
|
49
|
+
state.transitions.each do |transition|
|
50
50
|
events << transition.event
|
51
51
|
add_action(actions, transition.action)
|
52
52
|
end
|
@@ -175,9 +175,9 @@ module Statemachine
|
|
175
175
|
src << "{" << endl
|
176
176
|
src.indent!
|
177
177
|
add_one_liner(src, nil, "#{state_name}State", "#{@classname} statemachine", "super(statemachine)")
|
178
|
-
state.transitions
|
179
|
-
|
180
|
-
add_state_event_handler(
|
178
|
+
trans_aux = state.transitions
|
179
|
+
trans_aux.sort_by!{|t| t.event}.each do |t|
|
180
|
+
add_state_event_handler(t, src)
|
181
181
|
end
|
182
182
|
src.undent!
|
183
183
|
src << "}" << endl
|
@@ -1,39 +1,36 @@
|
|
1
1
|
module Statemachine
|
2
2
|
|
3
|
-
class Parallelstate< Superstate
|
4
|
-
|
5
|
-
attr_accessor :parallel_statemachines, :id
|
3
|
+
class Parallelstate < Superstate
|
4
|
+
|
5
|
+
attr_accessor :parallel_statemachines, :id, :entry_action, :exit_action
|
6
6
|
attr_reader :startstate_ids
|
7
7
|
|
8
8
|
def initialize(id, superstate, statemachine)
|
9
9
|
super(id, superstate, statemachine)
|
10
10
|
@parallel_statemachines=[]
|
11
11
|
@startstate_ids=[]
|
12
|
+
@transitions = []
|
13
|
+
@spontaneous_transitions = []
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# if (@parallel_statemachines.size>0 and @parallel_statemachines.size==@startstate_ids.size)
|
22
|
-
# return true
|
23
|
-
# end
|
24
|
-
# nil
|
25
|
-
# end
|
16
|
+
def add(transition)
|
17
|
+
if transition.event == nil
|
18
|
+
@spontaneous_transitions.push(transition)
|
19
|
+
else
|
20
|
+
@transitions.push(transition)
|
21
|
+
end
|
22
|
+
end
|
26
23
|
|
27
24
|
def context= c
|
28
25
|
@parallel_statemachines.each do |s|
|
29
|
-
|
26
|
+
s.context=c
|
30
27
|
end
|
31
28
|
end
|
32
29
|
|
33
30
|
def activate(terminal_state = nil)
|
34
31
|
@parallel_statemachines.each do |s|
|
35
|
-
|
36
|
-
@statemachine.activation.call(s.state,self.abstract_states,
|
32
|
+
next if terminal_state and s.has_state(terminal_state)
|
33
|
+
# @statemachine.activation.call(s.state,self.abstract_states+s.abstract_states,s.state) if @statemachine.activation
|
37
34
|
end
|
38
35
|
|
39
36
|
end
|
@@ -48,18 +45,21 @@ module Statemachine
|
|
48
45
|
|
49
46
|
def get_statemachine_with(id)
|
50
47
|
@parallel_statemachines.each do |s|
|
51
|
-
|
48
|
+
return s if s.has_state(id)
|
52
49
|
end
|
53
50
|
end
|
54
51
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
52
|
+
def get_transitions(event)
|
53
|
+
transitions = []
|
54
|
+
@transitions.each do |t|
|
55
|
+
if t.event == event
|
56
|
+
transitions << t
|
57
|
+
end
|
58
|
+
end
|
59
|
+
if transitions.empty?
|
60
|
+
return nil
|
61
|
+
end
|
62
|
+
transitions
|
63
63
|
end
|
64
64
|
|
65
65
|
def In(id)
|
@@ -70,12 +70,12 @@ module Statemachine
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def state= id
|
73
|
-
|
73
|
+
@parallel_statemachines.each do |s|
|
74
74
|
if s.has_state(id)
|
75
75
|
s.state=id
|
76
76
|
return true
|
77
77
|
end
|
78
|
-
|
78
|
+
end
|
79
79
|
return false
|
80
80
|
end
|
81
81
|
|
@@ -89,9 +89,6 @@ module Statemachine
|
|
89
89
|
end
|
90
90
|
|
91
91
|
def get_state(id)
|
92
|
-
# if state = @statemachine.get_state(id)
|
93
|
-
# return state
|
94
|
-
# end
|
95
92
|
@parallel_statemachines.each do |s|
|
96
93
|
if state = s.get_state(id)
|
97
94
|
return state
|
@@ -103,17 +100,24 @@ module Statemachine
|
|
103
100
|
def process_event(event, *args)
|
104
101
|
exceptions = []
|
105
102
|
result = false
|
106
|
-
# TODO fix needed: respond_to checks superstates lying out of the parallel state as well, in case an event is
|
107
|
-
# defined outside the parallel statemachine it gets processed twice!
|
108
103
|
|
109
|
-
|
104
|
+
# first check if the statemachine that currenlty executes the parallel state has a suitable transition
|
105
|
+
if (@statemachine.which_state_respond_to? event)
|
106
|
+
@statemachine.process_event(event,*args)
|
107
|
+
result = true
|
108
|
+
else # otherwise check for local transitions inside parallel state
|
109
|
+
|
110
|
+
@parallel_statemachines.each_with_index do |s,i|
|
111
|
+
t = s.which_state_respond_to? event
|
110
112
|
if s.respond_to? event
|
111
113
|
s.process_event(event,*args)
|
112
114
|
result = true
|
113
115
|
end
|
116
|
+
end
|
114
117
|
end
|
118
|
+
|
115
119
|
result
|
116
|
-
|
120
|
+
end
|
117
121
|
|
118
122
|
# Resets all of the statemachines back to theirs starting state.
|
119
123
|
def reset
|
@@ -137,11 +141,11 @@ module Statemachine
|
|
137
141
|
def substate_exiting(substate)
|
138
142
|
@history_id = substate.id
|
139
143
|
end
|
140
|
-
|
144
|
+
|
141
145
|
def add_substates(*substate_ids)
|
142
146
|
do_substate_adding(substate_ids)
|
143
147
|
end
|
144
|
-
|
148
|
+
|
145
149
|
def default_history=(state_id)
|
146
150
|
@history_id = @default_history_id = state_id
|
147
151
|
end
|
@@ -161,20 +165,28 @@ module Statemachine
|
|
161
165
|
end
|
162
166
|
|
163
167
|
def transition_for(event,check_superstates=true)
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
+
transition = super(event)
|
169
|
+
if not transition
|
170
|
+
@parallel_statemachines.each do |s|
|
171
|
+
transition = s.get_state(s.state).non_default_transition_for(event,false)
|
172
|
+
transition = s.get_state(s.state).default_transition if not transition
|
173
|
+
return transition if transition
|
174
|
+
end
|
175
|
+
@superstate.transition_for(event,check_superstates) if (@superstate and check_superstates and @superstate!=self)
|
176
|
+
else
|
177
|
+
transition
|
168
178
|
end
|
169
|
-
return @superstate.transition_for(event,check_superstates) if (@superstate and check_superstates and @superstate!=self)
|
170
|
-
|
171
|
-
#super.transition_for(event)
|
172
179
|
end
|
173
180
|
|
174
181
|
def enter(args=[])
|
175
|
-
|
176
|
-
#
|
177
|
-
|
182
|
+
@statemachine.state = self
|
183
|
+
@statemachine.trace("\tentering #{self}")
|
184
|
+
|
185
|
+
if @entry_action != nil
|
186
|
+
messenger = self.statemachine.messenger
|
187
|
+
message_queue = self.statemachine.message_queue
|
188
|
+
@statemachine.invoke_action(@entry_action, args, "entry action for #{self}", messenger, message_queue)
|
189
|
+
end
|
178
190
|
|
179
191
|
@parallel_statemachines.each_with_index do |s,i|
|
180
192
|
s.activation = @statemachine.activation
|
@@ -183,6 +195,39 @@ module Statemachine
|
|
183
195
|
end
|
184
196
|
end
|
185
197
|
|
198
|
+
def spontaneous_transition
|
199
|
+
transition = []
|
200
|
+
@parallel_statemachines.each do |s|
|
201
|
+
t = s.get_state(s.state).spontaneous_transition
|
202
|
+
transition << [t,s] if t # we need to store the state machine relevant for the transition
|
203
|
+
end
|
204
|
+
if transition.empty?
|
205
|
+
return nil
|
206
|
+
end
|
207
|
+
transition
|
208
|
+
end
|
209
|
+
|
210
|
+
|
211
|
+
def exit(args)
|
212
|
+
@statemachine.trace("\texiting #{self}")
|
213
|
+
|
214
|
+
if @exit_action != nil
|
215
|
+
messenger = self.statemachine.messenger
|
216
|
+
message_queue = self.statemachine.message_queue
|
217
|
+
@statemachine.invoke_action(@exit_action, args, "exit action for #{self}", messenger, message_queue)
|
218
|
+
@superstate.substate_exiting(self) if @superstate
|
219
|
+
end
|
220
|
+
|
221
|
+
@parallel_statemachines.each_with_index do |s,i|
|
222
|
+
as = s.get_state(s.state)
|
223
|
+
while as and as != self do
|
224
|
+
as.exit(args)
|
225
|
+
as = as.superstate
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
186
231
|
def to_s
|
187
232
|
return "'#{id}' parallel"
|
188
233
|
end
|
@@ -193,8 +238,11 @@ module Statemachine
|
|
193
238
|
if (@superstate)
|
194
239
|
abstract_states=@superstate.abstract_states
|
195
240
|
end
|
241
|
+
|
242
|
+
abstract_states += [@id]
|
243
|
+
|
196
244
|
@parallel_statemachines.each do |s|
|
197
|
-
abstract_states += s.abstract_states
|
245
|
+
abstract_states += s.abstract_states + []
|
198
246
|
end
|
199
247
|
abstract_states.uniq
|
200
248
|
end
|