enum_machine-contrib 0.1.0 → 1.0.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
  SHA256:
3
- metadata.gz: 8e7266c9b63527eb71ba12e52d236c39dfb32f7efb2119a7171181b535030e9b
4
- data.tar.gz: 1aff769ce18c3addf053e73bb7672727eda86ad3f4707dda11f45a0f50005900
3
+ metadata.gz: 35d405c0c7a47abceee4a0b4a970f2974249803b6caec8b6b5a335638b3d97fb
4
+ data.tar.gz: dc3710df781b062e8023f3b35d3f8e6e34629501b6e5b2423b6b49042759a9af
5
5
  SHA512:
6
- metadata.gz: 8135da25b19be77b3266baaa828d1e4e97bcf2cf8df76ceaca72c0cc394288b707629e39d6242c92446b6c4ff6b33e6e4dd39f5ef05ad0d6b875f5f15d706877
7
- data.tar.gz: 90c6af41a3aacfc09659647ce9af33114a58c09fd68b3560cb73c6fec35235ffe9c2c54f783674025db1a609e0675a0fd275b6631330d664f3fc4852ac0aab5b
6
+ metadata.gz: 2e5205c070cf6b358971938add7774d831234ed96c567434eb77e9f741f4aff23b3f174f754cf6c02896eebe5a42e66e90928151523e974408d62ade5f1552f2
7
+ data.tar.gz: f65b922b38231001950b9e7a9b3077d1e541a1aa497450929a95de327f468b9be6f05357f9c1e4bc19c14275b8e66f4c120d3450f62bede4ead8d15723fa8a27
data/.rubocop.yml CHANGED
@@ -19,4 +19,8 @@ Style/ClassVars:
19
19
 
20
20
  Naming/FileName:
21
21
  Exclude:
22
- - lib/enum_machine-contrib.rb
22
+ - lib/enum_machine-contrib.rb
23
+
24
+ Gp/ClassOrModuleDeclaredInWrongFile:
25
+ Exclude:
26
+ - lib/enum_machine_contrib/enum_machine/**/*.rb
data/Gemfile CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gemspec
6
-
7
5
  gem 'enum_machine', github: 'corp-gp/enum_machine'
8
6
  gem 'priscilla', github: 'corp-gp/priscilla'
9
7
  gem 'pry', '~> 0.12'
10
8
  gem 'rake', '~> 13.0'
11
9
  gem 'rspec', '~> 3.9'
12
10
  gem 'rubocop-gp', github: 'corp-gp/rubocop-gp'
11
+
12
+ gemspec
data/Gemfile.lock CHANGED
@@ -28,7 +28,7 @@ GIT
28
28
  PATH
29
29
  remote: .
30
30
  specs:
31
- enum_machine-contrib (0.1.0)
31
+ enum_machine-contrib (1.0.0)
32
32
  activesupport
33
33
  enum_machine
34
34
  ruby-graphviz
data/README.md CHANGED
@@ -6,22 +6,17 @@ This repository contains extensions and development tools for the [enum_machine]
6
6
 
7
7
  Install the gem and add to the application's Gemfile by executing:
8
8
 
9
- $ bundle add enum_machine-contrib ruby-graphviz --group "development"
9
+ $ bundle add enum_machine-contrib --group "development"
10
10
 
11
11
  If bundler is not being used to manage dependencies, install the gem by executing:
12
12
 
13
- $ gem install enum_machine-contrib ruby-graphviz
13
+ $ gem install enum_machine-contrib
14
14
 
15
15
  The gem depends on [GraphViz](https://graphviz.org/). See the [installation notes](https://graphviz.org/download/)
16
16
 
17
17
  ## Usage
18
18
 
19
- Suppose we have `Order` AR-model with the state machine specified by [enum_machine](https://github.com/corp-gp/enum_machine)
20
-
21
- `config/initializers/enum_machine.rb`
22
- ```ruby
23
- require 'enum_machine_contrib/enum_machine'
24
- ```
19
+ Suppose we have AR-model `Order` with the state machine specified by [enum_machine](https://github.com/corp-gp/enum_machine)
25
20
 
26
21
  `app/models/order.rb`
27
22
  ```ruby
@@ -50,11 +45,9 @@ class Order < ActiveRecord::Base
50
45
  end
51
46
  ```
52
47
 
53
- The gem allows you to get a visual representation of the graph of state transitions.
48
+ The gem allows to get a visual representation of the state's graph. Model name `Order` and attribute constant `STATE` should be defined in rake task to render image.
54
49
 
55
- ```ruby
56
- Order::STATE.machine.decision_tree.visualize.output(png: 'states.png')
57
- ```
50
+ $ bundle exec rake enum_machine:vis[Order::STATE]
58
51
 
59
52
  You will see:
60
53
 
data/README.ru.md ADDED
@@ -0,0 +1,114 @@
1
+ # Расширения EnumMachine
2
+
3
+ Репозиторий содержит дополнение к [enum_machine](https://github.com/corp-gp/enum_machine), которое позволяет генерировать графическое представление графа состояний. Для отображения используется open-source решение [Graphviz](https://graphviz.org/).
4
+
5
+ ## Установка
6
+
7
+ При использовании bundler добавить в Gemfile:
8
+
9
+ $ bundle add enum_machine-contrib --group "development"
10
+
11
+ Или установить gem в систему:
12
+
13
+ $ gem install enum_machine-contrib
14
+
15
+ Установку [GraphViz](https://graphviz.org/) смотрите в соответствующей [инструкции](https://graphviz.org/download/)
16
+
17
+ ## Использование
18
+
19
+ Предположим, имеется AR-модель `Order` с машиной состояний, заданной [enum_machine](https://github.com/corp-gp/enum_machine)
20
+
21
+ `app/models/order.rb`
22
+ ```ruby
23
+ class Order < ActiveRecord::Base
24
+ enum_machine :state, %w[created wait_for_send billed need_to_pay paid cancelled shipped lost received closed] do
25
+ transitions(
26
+ nil => 'created',
27
+ 'created' => %w[wait_for_send billed],
28
+ 'wait_for_send' => 'billed',
29
+ %w[created billed wait_for_send] => 'need_to_pay',
30
+ %w[created billed wait_for_send need_to_pay] => %w[paid cancelled],
31
+ %w[billed need_to_pay paid] => 'shipped',
32
+ %w[paid shipped] => 'lost',
33
+ %w[billed need_to_pay paid shipped] => 'received',
34
+ %w[paid shipped received] => 'closed',
35
+ )
36
+ end
37
+ end
38
+ ```
39
+
40
+ Графическое представление графа (`state`) можно получить rake-командой:
41
+
42
+ $ bundle exec rake enum_machine:vis[Order::STATE]
43
+
44
+ Результатом будет файл `tmp/order.png`:
45
+
46
+ ![states.png](docs/states.png?raw=true "states")
47
+
48
+ ## Дерево решения графа
49
+
50
+ Граф состояний представляется из себя направленный и возможно циклический граф, из начальной вершины которого можно попасть в одну из конечных. При сложных взаимосвязях даже графическое представление становится нагруженным для восприятия, особенно при наличии [зацикленных вершин](https://ru.wikipedia.org/wiki/%D0%A6%D0%B8%D0%BA%D0%BB_(%D0%B3%D1%80%D0%B0%D1%84)). Поэтому помимо ребер графа на рисунке выше имеется дополнительная разметка. Красными линиями отмечено дерево решения, в котором вершины соединены в [топологическом порядке](https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BF%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0). Простыми словами, требуется найти такой порядок вершин, в котором все ребра графа ведут из более ранней вершины в более позднюю.
51
+
52
+ Алгоритмы топологической сортировки для ориентированного ациклического графа известны. В ядре `ruby` есть встроенный модуль [TSort](https://ruby-doc.org/stdlib-3.0.0/libdoc/tsort/rdoc/TSort.html), использующй, в частности, алгоритм Тарьяна. `RubyOnRails` применяет топологическую сортировку для инициализации приложения. Каждый `Railtie` должен быть загружен не раньше, чем будут загружены все его зависимости.
53
+
54
+ Для циклического графа топологическая сортировка невозможна. На практике state-машина вряд ли будет представлять из себя полностью цикличный граф, где все вершины связаны со всеми. Скорее возможны островки [компонент сильной связности](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%BF%D0%BE%D0%BD%D0%B5%D0%BD%D1%82%D0%B0_%D1%81%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9_%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D0%BE%D1%81%D1%82%D0%B8). Это допущение позволяет сделать первое упрощение задачи построения дерева графа. Ищем в графе компоненты сильной связности, заменяем их на комбинированную вершину, а затем строим топологическую сортировку для комбинированных вершин.
55
+
56
+ ![strongly_connected_component.png](docs/strongly_connected_component.png?raw=true "strongly connected component")
57
+
58
+ В общем случае с циклическим подграфом сделать ничего не получится. Однако некоторые частные (но довольно частые), все же позволяют доопределить дерево решения.
59
+
60
+ ### Вершины с единственным входом
61
+
62
+ Среди вершин зацикленного графа могут оказаться те, которые имеют единственное входящее ребро. Это означает, что это ребро неминуемо должно появится в дереве решения. Следствием является несколько моментов:
63
+
64
+ 1) В вершину S2 есть едиственный путь из S1, из обеих вершин можно попасть в S4. Вершина S1 предшествует S2, значит более поздней зависимостью S4 будет S2, а переход S1 -> S4 не должен попасть в дерево решения.
65
+
66
+ ![move_forward_single.png](docs/move_forward_single.png?raw=true "move forward single")
67
+
68
+ 2) Это также справедливо для цепочек вершин. Если из S1 и S4 можно попасть в S5, то S1 -> S5 опять же можно игнорировать. Одноименные выходы "перетекают" в конец цепочки.
69
+
70
+ ![move_forward_chain.png](docs/move_forward_chain.png?raw=true "move forward chain")
71
+
72
+ 3) У первой вершины цепочки может быть несколько возможных входов. В некоторых случаях возможно определить, какой из них попадет в дерево решения. Если входящие ребра включают в себя достижимые в дальнейшем по цепочке вершины, можно попробовать их отбросить. Если после этого остается единственный вход - включаем его в дерево решения.
73
+
74
+ ![resolve_backward.png](docs/resolve_backward.png?raw=true "resolve backward")
75
+
76
+ ### bottleneck-вершины
77
+
78
+ Иногда в сильной компоненте графа может встретиться особый случай вершины, минуя которую, невозможно попасть из одной части графа в другую, все ребра сходятся через эту узловую точку. У этой вершины могут быть как прямые, так и обратные ребра, однако дерево решение должно пройти через эту вершину строго в направлении от входа. Для обнаружения такого варианта можно поочередно удалять вершины, проверяя достижимость всех вершин компоненты. Если в какой-то момент обнаружили, что часть подграфа недоступна - делим его по этой bottleneck-вершине (S5):
79
+
80
+ ![bottleneck.png](docs/bottleneck.png?raw=true "bottleneck")
81
+
82
+ 4) Отбрасываем входящие ребра, которые ведут из второй половины подграфа, недостижимой от входа.
83
+
84
+ ![bottleneck_incoming.png](docs/bottleneck_incoming.png?raw=true "bottleneck incoming")
85
+
86
+ 5) Из узловой вершины некоторые исходящие ребра будут вести в первую половину подграфа. По аналогии с вершинами с единственным входом, одноименные выходы "перетекают" на bottleneck-вершину.
87
+
88
+ ![bottleneck_outcoming.png](docs/bottleneck_outcoming.png?raw=true "bottleneck outcoming")
89
+
90
+ После применения описанных приемов часть дуг из подграфа удаляются и можно попробовать вновь поискать компоненты сильной связности. Алгоритм можно повторять до тех пор, пока не перестанет изменяться сложность графа (сумма активных ребер). Также для облегчения восприятия можно объединить вершины, которые имеют одинаковые входы/выходы и объединить двунаправленные ребра в одну. В результате получаем такую картинку:
91
+
92
+ ![bottleneck_resolved.png](docs/bottleneck_resolved.png?raw=true "bottleneck resolved")
93
+
94
+ ## Использованная литература
95
+
96
+ 1) Джефф Эриксон, Алгоритмы / пер. с англ. А. В. Снастина. – М.: ДМК Пресс, 2023.
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/enum_machine-contrib. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/enum_machine-contrib/blob/master/CODE_OF_CONDUCT.md).
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
111
+
112
+ ## Code of Conduct
113
+
114
+ Everyone interacting in the EnumMachine::Contrib project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/enum_machine-contrib/blob/master/CODE_OF_CONDUCT.md).
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/docs/states.png ADDED
Binary file
@@ -27,8 +27,6 @@ Gem::Specification.new do |spec|
27
27
  (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
28
28
  end
29
29
  end
30
- spec.bindir = 'exe'
31
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
30
  spec.require_paths = ['lib']
33
31
 
34
32
  spec.add_dependency 'activesupport'
@@ -22,7 +22,7 @@ module EnumMachineContrib
22
22
  to_vertex = vertex_by_value[to_value]
23
23
  @vertexes << to_vertex
24
24
 
25
- @edges << from_vertex.add_edge(to_vertex)
25
+ @edges << from_vertex.edge_to(to_vertex)
26
26
  end
27
27
  end
28
28
  end
@@ -63,7 +63,7 @@ module EnumMachineContrib
63
63
  vertexes.filter(&:active?).to_h do |vertex|
64
64
  [
65
65
  vertex.value,
66
- vertex.outcoming_edges.filter_map { |edge| edge.to.value if edge.active? },
66
+ vertex.outcoming_edges.map { |edge| edge.to.value },
67
67
  ]
68
68
  end
69
69
  end
@@ -93,50 +93,31 @@ module EnumMachineContrib
93
93
  end
94
94
 
95
95
  def resolve_strong_component!(component_cycled_vertex) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
96
- input_values = component_cycled_vertex.incoming_edges.filter(&:active?).flat_map { |edge| edge.from.value }
97
- output_values = component_cycled_vertex.outcoming_edges.filter(&:active?).flat_map { |vertex| vertex.to.value }
96
+ input_values = component_cycled_vertex.incoming_edges.flat_map { |edge| edge.from.value }
97
+ output_values = component_cycled_vertex.outcoming_edges.flat_map { |vertex| vertex.to.value }
98
98
 
99
99
  active_vertexes = vertexes.filter(&:active?)
100
- input_vertexes = active_vertexes.filter { |vertex| (input_values & vertex.value).any? }
101
- output_vertexes = active_vertexes.filter { |vertex| (output_values & vertex.value).any? }
100
+ input_vertexes = active_vertexes.reject { |vertex| (input_values & vertex.value).empty? }
101
+ output_vertexes = active_vertexes.reject { |vertex| (output_values & vertex.value).empty? }
102
102
 
103
- component_vertexes = active_vertexes.filter { |vertex| (component_cycled_vertex.value & vertex.value).any? }
103
+ component_vertexes = active_vertexes.reject { |vertex| (component_cycled_vertex.value & vertex.value).empty? }
104
104
 
105
- single_incoming_vertexes = (component_vertexes + output_vertexes).filter { |vertex| vertex.incoming_edges.size == 1 }
106
- single_incoming_vertexes.each do |to_vertex|
107
- from_vertex = to_vertex.incoming_edges.first.from
108
-
109
- (from_vertex.incoming_edges + from_vertex.outcoming_edges)
110
- .group_by { |edge| [edge.from.value.to_s, edge.to.value.to_s].sort }
111
- .values
112
- .filter { |current_edges| current_edges.size > 1 }.each do |current_edges|
113
- current_edges.each do |edge|
114
- # S1 -> S2; S2 -> [S1, S3]
115
- # drops back reference S2 -> S1
116
- edge.dropped! if edge.from == from_vertex
117
- end
105
+ component_vertexes.each do |vertex|
106
+ vertex.incoming_edges.each do |edge|
107
+ if (input_vertexes + component_vertexes).exclude?(edge.from)
108
+ # drop insignificant incoming edges
109
+ edge.dropped!
118
110
  end
111
+ end
119
112
  end
120
113
 
121
- resolved_not_visited_vertexes = []
122
-
123
- current_vertexes = [component_cycled_vertex]
124
- loop do
125
- next_vertexes = current_vertexes.flat_map { |vertex| vertex.outcoming_edges.filter_map { |edge| edge.to if edge.active? } }
126
- resolved_not_visited_vertexes += next_vertexes.reject(&:cycled?)
127
- current_vertexes = next_vertexes
128
- break if next_vertexes.empty?
129
- end
114
+ single_incoming_vertexes = (component_vertexes + output_vertexes).filter { |vertex| vertex.incoming_edges.size == 1 }
130
115
 
131
116
  single_incoming_chains = []
132
117
  single_incoming_vertexes.each do |vertex|
133
118
  single_incoming_edge = vertex.incoming_edges.first
134
- # S1 -> [S2, S3]; S2 -> S3
135
- # resolve S1 -> S2 because it is only one path to S2
136
119
  single_incoming_edge.resolved!
137
120
 
138
- resolved_not_visited_vertexes << single_incoming_edge.to
139
-
140
121
  current_chain = single_incoming_chains.detect { |chain| chain.first == single_incoming_edge.to || chain.last == single_incoming_edge.from }
141
122
  if current_chain
142
123
  current_chain.replace(
@@ -152,48 +133,69 @@ module EnumMachineContrib
152
133
  end
153
134
 
154
135
  single_incoming_chains.each do |chain|
136
+ chain_preceding_vertexes = chain[0].incoming_edges.map(&:from) & (input_vertexes + component_vertexes)
137
+ chain_achievable_vertexes = chain[1..].flat_map { |vertex| vertex.outcoming_edges.map(&:to) }
138
+
139
+ pre_chain_vertexes = chain_preceding_vertexes - chain_achievable_vertexes
140
+
141
+ if pre_chain_vertexes.size == 1
142
+ single_incoming_edge = chain[0].incoming_edges.detect { |edge| edge.from == pre_chain_vertexes[0] }
143
+ single_incoming_edge.resolved!
144
+
145
+ chain.unshift(single_incoming_edge.from)
146
+ end
147
+
155
148
  chain.flat_map { |vertex| vertex.outcoming_edges.to_a }.group_by(&:to).each_value do |outcoming_edges|
156
149
  next if outcoming_edges.size < 2
157
150
 
158
- # S1 -> [S2, S3]; S2 -> S3
159
- # drops S1 -> S3, because S1 -> S2 resolved as only one path and S3 is reachable from S2
160
151
  outcoming_edges[0..outcoming_edges.size - 2].each(&:dropped!)
161
152
  end
162
153
  end
163
154
 
164
- current_vertexes = input_vertexes
165
- visited_vertexes = []
166
- loop do
167
- current_following_vertexes = current_vertexes.flat_map { |vertex| vertex.outcoming_edges.map(&:to) }.uniq - visited_vertexes
155
+ around_vertexes = input_vertexes + component_vertexes + output_vertexes
156
+
157
+ component_vertexes.each do |maybe_bottlneck_vertex|
158
+ rest_vertexes = around_vertexes.excluding(maybe_bottlneck_vertex)
168
159
 
169
- if current_following_vertexes.size > 1
170
- reachable_vertexes = resolved_not_visited_vertexes.flat_map { |vertex| vertex.outcoming_edges.map(&:to) }.uniq
171
- break if reachable_vertexes.empty?
160
+ input_achievable_vertexes = next_achievable_vertexes(input_vertexes[0], rest_vertexes, visited: Set.new([maybe_bottlneck_vertex]))
161
+ next if input_achievable_vertexes.size == rest_vertexes.size
172
162
 
173
- current_following_vertexes -= reachable_vertexes
163
+ maybe_bottlneck_vertex.outcoming_edges.each do |current_edge|
164
+ if input_achievable_vertexes.include?(current_edge.to) && maybe_bottlneck_vertex.incoming_edges.map(&:from).exclude?((current_edge.to))
165
+ current_edge.to.incoming_edges.each do |edge|
166
+ unless edge.from == maybe_bottlneck_vertex
167
+ edge.dropped!
168
+ end
169
+ end
170
+ end
174
171
  end
175
172
 
176
- current_vertexes.each do |from_vertex|
177
- from_vertex.outcoming_edges.each do |edge|
178
- next if edge.resolved?
173
+ rest_vertexes -= input_achievable_vertexes
179
174
 
180
- if current_following_vertexes.include?(edge.to)
181
- edge.resolved! if current_following_vertexes.size == 1
182
- elsif resolved_not_visited_vertexes.include?(edge.to)
183
- edge.dropped!
184
- end
175
+ maybe_bottlneck_vertex.incoming_edges.each do |edge|
176
+ if rest_vertexes.include?(edge.from)
177
+ edge.dropped!
185
178
  end
186
179
  end
180
+ end
181
+ end
182
+
183
+ def next_achievable_vertexes(vertex, all, visited: Set.new)
184
+ return [] if visited.include?(vertex)
187
185
 
188
- current_vertexes = current_following_vertexes
189
- resolved_not_visited_vertexes -= current_following_vertexes
190
- visited_vertexes += current_vertexes
186
+ achievable_vertexes = [vertex]
187
+ visited << vertex
191
188
 
192
- break if current_vertexes.empty?
189
+ vertex.outcoming_edges.each do |edge|
190
+ if all.include?(edge.to)
191
+ achievable_vertexes += next_achievable_vertexes(edge.to, all, visited: visited)
192
+ end
193
193
  end
194
+
195
+ achievable_vertexes
194
196
  end
195
197
 
196
- def array_wrap(value)
198
+ private def array_wrap(value)
197
199
  if value.nil?
198
200
  [nil]
199
201
  else
@@ -8,7 +8,7 @@ module EnumMachineContrib
8
8
  include TSort
9
9
 
10
10
  def tsort_each_child(node, &_block)
11
- fetch(node).outcoming_edges.each { |edge| yield(edge.to.value) if edge.active? }
11
+ fetch(node).outcoming_edges.each { |edge| yield(edge.to.value) }
12
12
  end
13
13
 
14
14
  def tsort_each_node(&_block)
@@ -24,7 +24,7 @@ module EnumMachineContrib
24
24
 
25
25
  to_value_list.each do |to_value|
26
26
  vertex_by_value[to_value] ||= Vertex[to_value]
27
- from_vertex.add_edge(vertex_by_value[to_value])
27
+ from_vertex.edge_to(vertex_by_value[to_value])
28
28
  end
29
29
  end
30
30
 
@@ -73,7 +73,7 @@ module EnumMachineContrib
73
73
  each_value do |vertex|
74
74
  next unless vertex.active?
75
75
 
76
- resolved_hash[vertex.value] = vertex.outcoming_edges.filter_map { |edge| edge.to.value if edge.active? }
76
+ resolved_hash[vertex.value] = vertex.outcoming_edges.map { |edge| edge.to.value }
77
77
  end
78
78
 
79
79
  resolved_hash
@@ -110,8 +110,11 @@ module EnumMachineContrib
110
110
 
111
111
  vertexes_by_level = visible_vertexes.filter(&:level).sort_by(&:level).group_by(&:level)
112
112
  node_ranks =
113
- vertexes_by_level.map do |_level, vertexes_same_rank|
114
- "{ rank=same #{vertexes_same_rank.reject(&:combined?).reject(&:cycled?).map { |vertex| nodes[vertex][:id] }.join(' ')} }"
113
+ vertexes_by_level.filter_map do |_level, vertexes_same_rank|
114
+ vertex_ids = vertexes_same_rank.filter_map { |vertex| nodes[vertex][:id] if !vertex.cycled? && !vertex.combined? }
115
+ next if vertex_ids.empty?
116
+
117
+ "{ rank=same #{vertex_ids.join(' ')} }"
115
118
  end
116
119
 
117
120
  node_labels = plain_vertexes.map { |vertex| "#{nodes[vertex][:id]} [label=\"#{nodes[vertex][:label]}\"]" }
@@ -121,26 +124,62 @@ module EnumMachineContrib
121
124
  "subgraph #{nodes[vertex][:cluster_id]} { color=blue style=dashed #{vertex.value.join(' ')} }"
122
125
  end
123
126
 
124
- transitions =
127
+ resolved_not_active_edges = []
128
+
129
+ pending_edges =
125
130
  visible_vertexes.flat_map do |vertex|
126
- vertex.outcoming_edges.filter_map do |edge|
131
+ vertex.incoming_edges.with_dropped.filter_map do |edge|
127
132
  if (!edge.from.combined? && (combined_values & edge.from.value).any?) ||
128
133
  (!edge.to.combined? && (combined_values & edge.to.value).any?)
129
134
  next
130
135
  end
131
136
 
132
- attrs = []
133
- if edge.active?
134
- attrs << ACTIVE_EDGE_STYLE
135
- attrs << "ltail=#{nodes[edge.from][:cluster_id]}" if edge.from.cycled?
136
- attrs << "lhead=#{nodes[edge.to][:cluster_id]}" if edge.to.cycled?
137
- else
138
- attrs << INACTIVE_EDGE_STYLE
137
+ if !edge.active? && (edge.from.combined? || edge.to.combined? || edge.from.cycled? || edge.to.cycled?)
138
+ next
139
+ end
140
+
141
+ if edge.resolved? && !edge.active?
142
+ resolved_not_active_edges << edge
139
143
  end
140
- "#{nodes[edge.from][:id]} -> #{nodes[edge.to][:id]} [#{attrs.join(' ')}]"
144
+
145
+ edge
141
146
  end
142
147
  end
143
148
 
149
+ resolved_not_active_edges.each do |current_edge|
150
+ pending_edges.delete_if do |edge|
151
+ (edge.from.cycled? && (edge.from.value & current_edge.from.value).any? && edge.to == current_edge.to) ||
152
+ (edge.from == current_edge.from && edge.to.cycled? && (edge.to.value & current_edge.to.value).any?)
153
+ end
154
+ end
155
+
156
+ transitions = []
157
+
158
+ until pending_edges.empty?
159
+ current_edge = pending_edges.shift
160
+
161
+ attrs = []
162
+
163
+ if current_edge.resolved?
164
+ attrs << ACTIVE_EDGE_STYLE
165
+ attrs << "ltail=#{nodes[current_edge.from][:cluster_id]}" if current_edge.from.cycled?
166
+ attrs << "lhead=#{nodes[current_edge.to][:cluster_id]}" if current_edge.to.cycled?
167
+ else
168
+ attrs << INACTIVE_EDGE_STYLE
169
+ end
170
+
171
+ unless current_edge.resolved?
172
+ reverse_edge = pending_edges.detect { |edge| edge.from == current_edge.to && edge.to == current_edge.from }
173
+
174
+ if reverse_edge && !reverse_edge.resolved?
175
+ pending_edges.delete(reverse_edge)
176
+ attrs << 'dir=both'
177
+ end
178
+ end
179
+
180
+ transitions << "#{nodes[current_edge.from][:id]} -> #{nodes[current_edge.to][:id]} [#{attrs.join(' ')}]"
181
+ end
182
+
144
183
  <<~DOT
145
184
  digraph {
146
185
  ranksep="1.0 equally"
@@ -6,12 +6,13 @@ module EnumMachineContrib
6
6
  Struct.new(:from, :to) do
7
7
  attr_accessor :mode
8
8
 
9
- EDGE_MODES = %i[pending resolved dropped].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
9
+ EDGE_MODES = %i[pending dropped].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
10
10
 
11
11
  def initialize(from, to)
12
12
  self.from = from
13
13
  self.to = to
14
14
 
15
+ @resolved = false
15
16
  pending!
16
17
  end
17
18
 
@@ -37,16 +38,31 @@ module EnumMachineContrib
37
38
  !dropped?
38
39
  end
39
40
 
41
+ def dropped!
42
+ self.mode = :dropped
43
+
44
+ to.incoming_edges.delete(self)
45
+ from.outcoming_edges.delete(self)
46
+ end
47
+
48
+ def resolved?
49
+ @resolved == true
50
+ end
51
+
40
52
  def resolved!
41
- self.mode = :resolved
53
+ @resolved = true
42
54
 
43
55
  to.incoming_edges.each do |edge|
44
56
  edge.dropped! unless edge == self
45
57
  end
58
+
59
+ to.outcoming_edges.detect { |edge| edge.to == from }&.dropped!
60
+
61
+ to.resolved!
46
62
  end
47
63
 
48
64
  def inspect
49
- "<Edge [#{mode}] #{from.inspect} -> #{to.inspect}>"
65
+ "<Edge [#{mode}]#{'[resolved]' if resolved?} #{from.inspect} -> #{to.inspect}>"
50
66
  end
51
67
  end
52
68
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnumMachineContrib
4
+ class EdgeSet < Set
5
+
6
+ attr_accessor :with_dropped
7
+
8
+ def initialize(*)
9
+ @with_dropped = Set.new
10
+ super
11
+ end
12
+
13
+ def add(value)
14
+ @with_dropped << value
15
+ super
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnumMachine
4
+ class InvalidTransitionGraph < Error; end
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnumMachineContrib
4
+ class Railtie < Rails::Railtie
5
+
6
+ rake_tasks do
7
+ path = File.expand_path(__dir__)
8
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :enum_machine do
4
+ desc 'Visualize graph for enum_machine attribute with enum_machine:vis[Order::STATE]'
5
+ task :vis, [:attr] => :environment do |_t, args|
6
+ require 'ruby-graphviz'
7
+ require 'fileutils'
8
+
9
+ FileUtils.mkdir_p('tmp')
10
+
11
+ enum_machine = args[:attr].constantize.machine
12
+ enum_machine.singleton_class.include(EnumMachineContrib::HasDecisionTree)
13
+
14
+ file_path = "./tmp/#{enum_machine.base_klass.name.demodulize.underscore}.png"
15
+ enum_machine.decision_tree.visualize.output(png: file_path)
16
+
17
+ puts <<~TEXT
18
+ Rendered to #{file_path}, open in browser with:#{' '}
19
+ xdg-open file://#{File.expand_path(file_path)}
20
+ TEXT
21
+ end
22
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module EnumMachineContrib
4
4
 
5
- VERSION = '0.1.0'
5
+ VERSION = '1.0.0'
6
6
 
7
7
  end
@@ -4,16 +4,19 @@ module EnumMachineContrib
4
4
 
5
5
  Vertex =
6
6
  Struct.new(:value) do
7
- attr_accessor :mode, :incoming_edges, :outcoming_edges, :level
7
+ attr_accessor :mode, :level
8
+ attr_reader :incoming_edges, :outcoming_edges
8
9
 
9
- VERTEX_MODES = %i[plain dropped combined cycled].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
10
+ VERTEX_MODES = %i[pending dropped combined cycled].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
10
11
 
11
12
  def initialize(value)
12
13
  self.value = value
13
- self.outcoming_edges = Set.new
14
- self.incoming_edges = Set.new
15
14
 
16
- plain!
15
+ @incoming_edges = EdgeSet.new
16
+ @outcoming_edges = EdgeSet.new
17
+
18
+ @resolved = false
19
+ pending!
17
20
  end
18
21
 
19
22
  VERTEX_MODES.each do |mode|
@@ -38,6 +41,14 @@ module EnumMachineContrib
38
41
  !dropped?
39
42
  end
40
43
 
44
+ def resolved!
45
+ @resolved = true
46
+ end
47
+
48
+ def resolved?
49
+ @resolved == true
50
+ end
51
+
41
52
  def dropped!
42
53
  return if dropped?
43
54
 
@@ -47,11 +58,11 @@ module EnumMachineContrib
47
58
  outcoming_edges.each(&:dropped!)
48
59
  end
49
60
 
50
- def add_edge(to_vertex)
61
+ def edge_to(to_vertex)
51
62
  new_edge = Edge.new(self, to_vertex)
52
63
 
53
- outcoming_edges << new_edge
54
- to_vertex.incoming_edges << new_edge
64
+ outcoming_edges.add(new_edge)
65
+ to_vertex.incoming_edges.add(new_edge)
55
66
 
56
67
  new_edge
57
68
  end
@@ -61,17 +72,15 @@ module EnumMachineContrib
61
72
 
62
73
  replacing_vertexes.each do |replacing_vertex|
63
74
  replacing_vertex.incoming_edges.each do |edge|
64
- next unless edge.active?
65
75
  next if replacing_vertexes.include?(edge.from)
66
76
 
67
- edge.from.add_edge(new_vertex)
77
+ edge.from.edge_to(new_vertex)
68
78
  end
69
79
 
70
80
  replacing_vertex.outcoming_edges.filter_map do |edge|
71
- next unless edge.active?
72
81
  next if replacing_vertexes.include?(edge.to)
73
82
 
74
- new_vertex.add_edge(edge.to)
83
+ new_vertex.edge_to(edge.to)
75
84
  end
76
85
 
77
86
  replacing_vertex.dropped!
@@ -81,7 +90,7 @@ module EnumMachineContrib
81
90
  end
82
91
 
83
92
  def inspect
84
- "<Vertex [#{mode}] value=#{value || 'nil'}>"
93
+ "<Vertex [#{mode}]#{'[resolved]' if resolved?} value=#{value || 'nil'}>"
85
94
  end
86
95
  end
87
96
 
@@ -5,6 +5,8 @@ require 'active_support/core_ext/enumerable'
5
5
  require 'active_support/core_ext/array/wrap'
6
6
 
7
7
  require 'enum_machine_contrib/version'
8
+ require 'enum_machine_contrib/railtie' if defined?(Rails::Railtie)
9
+ require 'enum_machine_contrib/enum_machine/errors'
8
10
 
9
11
  module EnumMachineContrib
10
12
 
@@ -13,5 +15,6 @@ module EnumMachineContrib
13
15
  autoload :DecisionTree, 'enum_machine_contrib/decision_tree'
14
16
  autoload :Vertex, 'enum_machine_contrib/vertex'
15
17
  autoload :Edge, 'enum_machine_contrib/edge'
18
+ autoload :EdgeSet, 'enum_machine_contrib/edge_set'
16
19
 
17
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: enum_machine-contrib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergei Malykh
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-14 00:00:00.000000000 Z
11
+ date: 2023-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -68,18 +68,30 @@ files:
68
68
  - LICENSE
69
69
  - LICENSE.txt
70
70
  - README.md
71
+ - README.ru.md
71
72
  - Rakefile
73
+ - docs/bottleneck.png
74
+ - docs/bottleneck_incoming.png
75
+ - docs/bottleneck_outcoming.png
76
+ - docs/bottleneck_resolved.png
77
+ - docs/move_forward_chain.png
78
+ - docs/move_forward_single.png
79
+ - docs/resolve_backward.png
80
+ - docs/states.png
81
+ - docs/strongly_connected_component.png
72
82
  - enum_machine-contrib.gemspec
73
83
  - lib/enum_machine-contrib.rb
74
84
  - lib/enum_machine_contrib.rb
75
85
  - lib/enum_machine_contrib/decision_graph.rb
76
86
  - lib/enum_machine_contrib/decision_tree.rb
77
87
  - lib/enum_machine_contrib/edge.rb
78
- - lib/enum_machine_contrib/enum_machine.rb
88
+ - lib/enum_machine_contrib/edge_set.rb
89
+ - lib/enum_machine_contrib/enum_machine/errors.rb
79
90
  - lib/enum_machine_contrib/has_decision_tree.rb
91
+ - lib/enum_machine_contrib/railtie.rb
92
+ - lib/enum_machine_contrib/tasks/enum_machine/vis.rake
80
93
  - lib/enum_machine_contrib/version.rb
81
94
  - lib/enum_machine_contrib/vertex.rb
82
- - states.png
83
95
  homepage: https://github.com/corp-gp/enum_machine-contrib
84
96
  licenses:
85
97
  - MIT
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module EnumMachine
4
-
5
- class Machine
6
-
7
- include EnumMachineContrib::HasDecisionTree
8
-
9
- end
10
-
11
- class InvalidTransitionGraph < StandardError; end
12
-
13
- end
data/states.png DELETED
Binary file