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 +4 -4
- data/.rubocop.yml +5 -1
- data/Gemfile +2 -2
- data/Gemfile.lock +1 -1
- data/README.md +5 -12
- data/README.ru.md +114 -0
- data/docs/bottleneck.png +0 -0
- data/docs/bottleneck_incoming.png +0 -0
- data/docs/bottleneck_outcoming.png +0 -0
- data/docs/bottleneck_resolved.png +0 -0
- data/docs/move_forward_chain.png +0 -0
- data/docs/move_forward_single.png +0 -0
- data/docs/resolve_backward.png +0 -0
- data/docs/states.png +0 -0
- data/docs/strongly_connected_component.png +0 -0
- data/enum_machine-contrib.gemspec +0 -2
- data/lib/enum_machine_contrib/decision_graph.rb +58 -56
- data/lib/enum_machine_contrib/decision_tree.rb +54 -15
- data/lib/enum_machine_contrib/edge.rb +19 -3
- data/lib/enum_machine_contrib/edge_set.rb +19 -0
- data/lib/enum_machine_contrib/enum_machine/errors.rb +5 -0
- data/lib/enum_machine_contrib/railtie.rb +12 -0
- data/lib/enum_machine_contrib/tasks/enum_machine/vis.rake +22 -0
- data/lib/enum_machine_contrib/version.rb +1 -1
- data/lib/enum_machine_contrib/vertex.rb +22 -13
- data/lib/enum_machine_contrib.rb +3 -0
- metadata +17 -5
- data/lib/enum_machine_contrib/enum_machine.rb +0 -13
- data/states.png +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35d405c0c7a47abceee4a0b4a970f2974249803b6caec8b6b5a335638b3d97fb
|
4
|
+
data.tar.gz: dc3710df781b062e8023f3b35d3f8e6e34629501b6e5b2423b6b49042759a9af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e5205c070cf6b358971938add7774d831234ed96c567434eb77e9f741f4aff23b3f174f754cf6c02896eebe5a42e66e90928151523e974408d62ade5f1552f2
|
7
|
+
data.tar.gz: f65b922b38231001950b9e7a9b3077d1e541a1aa497450929a95de327f468b9be6f05357f9c1e4bc19c14275b8e66f4c120d3450f62bede4ead8d15723fa8a27
|
data/.rubocop.yml
CHANGED
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
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
|
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
|
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`
|
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
|
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
|
-
|
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).
|
data/docs/bottleneck.png
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/docs/states.png
ADDED
Binary file
|
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.
|
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.
|
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.
|
97
|
-
output_values = component_cycled_vertex.outcoming_edges.
|
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
|
101
|
-
output_vertexes = active_vertexes.
|
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.
|
103
|
+
component_vertexes = active_vertexes.reject { |vertex| (component_cycled_vertex.value & vertex.value).empty? }
|
104
104
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
|
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
|
-
|
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
|
-
|
177
|
-
from_vertex.outcoming_edges.each do |edge|
|
178
|
-
next if edge.resolved?
|
173
|
+
rest_vertexes -= input_achievable_vertexes
|
179
174
|
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
189
|
-
|
190
|
-
visited_vertexes += current_vertexes
|
186
|
+
achievable_vertexes = [vertex]
|
187
|
+
visited << vertex
|
191
188
|
|
192
|
-
|
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)
|
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.
|
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.
|
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.
|
114
|
-
|
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
|
-
|
127
|
+
resolved_not_active_edges = []
|
128
|
+
|
129
|
+
pending_edges =
|
125
130
|
visible_vertexes.flat_map do |vertex|
|
126
|
-
vertex.
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
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
|
-
|
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,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
|
@@ -4,16 +4,19 @@ module EnumMachineContrib
|
|
4
4
|
|
5
5
|
Vertex =
|
6
6
|
Struct.new(:value) do
|
7
|
-
attr_accessor :mode, :
|
7
|
+
attr_accessor :mode, :level
|
8
|
+
attr_reader :incoming_edges, :outcoming_edges
|
8
9
|
|
9
|
-
VERTEX_MODES = %i[
|
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
|
-
|
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
|
61
|
+
def edge_to(to_vertex)
|
51
62
|
new_edge = Edge.new(self, to_vertex)
|
52
63
|
|
53
|
-
outcoming_edges
|
54
|
-
to_vertex.incoming_edges
|
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.
|
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.
|
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
|
|
data/lib/enum_machine_contrib.rb
CHANGED
@@ -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:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergei Malykh
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
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/
|
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
|
data/states.png
DELETED
Binary file
|