Class: TagHierarchyBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/tag_hierarchy_builder.rb

Defined Under Namespace

Classes: WrongSpecificationSyntax

Class Method Summary collapse

Class Method Details

.dump_hierarchyObject



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/tag_hierarchy_builder.rb', line 157

def self.dump_hierarchy
  tags_chains = Tag.with_joined_hierarchy.without_children.with_parents.find(:all).map { |x| [x] }

  # OPTIMIZE
  while chain = tags_chains.detect { |chain| !chain.first.parents.empty? }
    tags_chains.delete(chain)
    chain.first.parents.each do |parent|
      raise Tag::HierarchyCycle.new if chain.include?(parent)
      tags_chains << ([parent] + chain)
    end
  end

  tags_chains.map { |chain| chain.map { |tag| tag.name } }.sort_by { |chain| chain * ' ' }
end

.dump_orphansObject



181
182
183
184
185
# File 'lib/tag_hierarchy_builder.rb', line 181

def self.dump_orphans
  # OPTIMIZE
  Tag.with_joined_hierarchy_and_synonyms.without_children.without_parents.without_synonyms.
    pluck(:name)
end

.dump_synonymsObject



172
173
174
175
176
177
178
179
# File 'lib/tag_hierarchy_builder.rb', line 172

def self.dump_synonyms
  # OPTIMIZE N+1
  tags_with_synonyms = Tag.includes(:synonyms)

  tags_with_synonyms.map { |t|
    [ t.name, *t.synonyms.pluck(:name).sort ]
  }.keep_if(&:second)
end

.dump_tagsObject

DUMPER ======



148
149
150
151
152
153
154
155
# File 'lib/tag_hierarchy_builder.rb', line 148

def self.dump_tags
  ['# Categories'] +
  dump_hierarchy.map { |chain| chain * ' / '} +
  ['', '# Synonyms'] +
  dump_synonyms.map { |chain| chain * ' = '} +
  ['', '# Unlinked tags'] +
  dump_orphans
end

.hierarchy_acyclic?Boolean



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/tag_hierarchy_builder.rb', line 125

def self.hierarchy_acyclic?
  # OPTIMIZE
  tags = Tag.includes(:children)

  tags_status = tags.map { |x| [ x, :unvisited ] }.flatten
  tags_status = Hash[*tags_status]

  tags.each do |tag|
    next if tags_status[tag] != :unvisited
    (reclambda do |this, tag|
      return false if tags_status[tag] == :processing
      tags_status[tag] = :processing
      tag.children.any? do |child|
        this.call(child)
      end
      tags_status[tag] = :closed
    end).call(tag)
  end
  return true
end

.instantiate_hierarchy(line) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/tag_hierarchy_builder.rb', line 112

def self.instantiate_hierarchy(line)
  line = line.split('/').map(&:strip)

  line.each_cons(2) do |(p, c)|
    p = Tag.find_or_create_with_like_by_name!(p)
    c = Tag.find_or_create_with_like_by_name!(c)

    raise Tag::HierarchyCycle.new if c.parents.include?(p) || c == p

    c.parents << p
  end
end

.instantiate_synonyms(line) ⇒ Object

Input should be validated



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/tag_hierarchy_builder.rb', line 92

def self.instantiate_synonyms(line)
  # (2) TODO validate synonyms repetition? Like Cat = Kitty and Kitty = Cat
  # То есть ни один из «синонимов» не может быть в «стволе»
  # (3) FIXME а ведь можно намутить цикл при помощи сочетания синонимов и иерархии.
  # Ни один из синонимов не может участвовать в «иерархии»
  # То есть нам нужна flat иерархия, flat ствол и flat синонимы
  # Причём хранить с номерами строк и выдавать ошибки «в такой-то строке»
  # (4) TODO Мы хотим гламурные сообщения о циклах. Для этого опять-таки нужно можно воспользоваться тем flatten.
  # Итого тесты. Сообщения об ошибках:
  # "Левый синтаксис в строке 5"
  # "Синоним AAA из строки 15 участвует в иерархии в строках …"
  # "Синоним BBB из строки 15 повторяется в строке …"
  syns = line.split('=').map(&:strip)

  b = Tag.find_or_create_with_like_by_name!(syns.shift)
  syns.each do |syn|
    b.synonyms << Tag.find_or_create_with_like_by_name!(syn)
  end
end

.rebuild_hierarchy(specification) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/tag_hierarchy_builder.rb', line 58

def self.rebuild_hierarchy(specification)
  # TODO save old hierarchy somewhere.

  Tag.transaction do
    Tag.connection.execute('DELETE from tags_hierarchy')
    Tag.connection.execute('DELETE from tags_synonyms')

    specification.each do |line|
      next if line.blank?
      next if line =~ /^\s*#.*/ # If line is a comment
      next if line =~ /^\s*#{Tag::SYMBOL}+\s*$/ # If line is a single tag
      begin
        if line =~ /^\s*#{Tag::SYMBOL}+\s*(=\s*#{Tag::SYMBOL}+\s*)+$/
          instantiate_synonyms(line)
          next
        end

        if line =~ /^\s*#{Tag::SYMBOL}+\s*(\/\s*#{Tag::SYMBOL}+\s*)+$/
          instantiate_hierarchy(line)
          next
        end

        raise WrongSpecificationSyntax.new("Line #{line}")
      rescue ActiveRecord::RecordInvalid => _
        raise WrongSpecificationSyntax.new("Line #{line}")
      end
    end

    hierarchy_acyclic? or raise Tag::HierarchyCycle
    rebuild_transitive_closure
  end
end

.rebuild_transitive_closureObject



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/tag_hierarchy_builder.rb', line 4

def self.rebuild_transitive_closure
  Tag.transaction do
    Tag.connection.execute('DELETE from tags_transitive_hierarchy')

    # (0) — тесты!
    # OPTIMIZE
    tags = Tag.includes(:synonyms, :children, :transitive_children)
    transitive_children = { }

    tags.each do |tag|
      transitive_children[tag] = []
      tag.children.each do |tag_child|
        transitive_children[tag] << tag_child
      end
    end

    visited_tags = []
    root = {} ; comp = [] ; stack = []
    tags.each do |outer_tag|
      next if visited_tags.include?(outer_tag)
      reclambda do |this, tag|
        visited_tags << tag
        root[tag] = tag; stack.push(tag)
        tag.children.each do |child|
          this.call(child) unless visited_tags.include?(child)
          if !comp.include?(child)
            root[tag] = visited_tags.index(tag) < visited_tags.index(child) ? tag : child
          end
          transitive_children[tag] += transitive_children[child]
        end
        if root[tag] == tag
          loop do
            w = stack.pop
            comp << w
            transitive_children[w] = transitive_children[tag] # Pointer assignment!
            break if w == tag
          end
        end
      end.call(outer_tag)
    end

    tags.each do |tag|
      tag.synonyms.each do |synonym|
        transitive_children[tag] << synonym unless transitive_children[tag].include?(synonym)
        transitive_children[synonym] << tag unless transitive_children[synonym].include?(tag)
      end
    end

    tags.each do |tag|
      tag.transitive_children = transitive_children[tag].uniq
    end
  end
end