rubyfca 0.2.11 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -1
- data/Gemfile +3 -3
- data/LICENSE +674 -20
- data/README.md +129 -0
- data/Rakefile +11 -0
- data/bin/rubyfca +59 -53
- data/lib/rubyfca/ruby_graphviz.rb +20 -23
- data/lib/rubyfca/version.rb +3 -1
- data/lib/rubyfca.rb +147 -141
- data/rubyfca.gemspec +18 -10
- data/samples/fca-source-result.png +0 -0
- data/samples/numbers-result-compact.png +0 -0
- data/samples/numbers-result.png +0 -0
- data/samples/numbers-source.png +0 -0
- data/samples/sample_01.svg +202 -0
- data/samples/sample_02.svg +178 -0
- data/samples/sample_03.svg +244 -0
- data/samples/xlsx-sample.png +0 -0
- data/test/rubyfca_test.rb +55 -4
- data/test/test_expected/test_result_01.svg +202 -0
- data/test/test_expected/test_result_02.svg +178 -0
- data/test/test_expected/test_result_03.svg +481 -0
- data/test/test_expected/test_result_04.svg +314 -0
- data/test/test_input/test_data.csv +7 -0
- data/test/{test_data.cxt → test_input/test_data.cxt} +1 -1
- data/test/test_input/test_data.xlsx +0 -0
- data/test/test_input/test_data_numbers.xlsx +0 -0
- data/test_data_numbers.svg +272 -0
- data/test_data_numbers_a.svg +272 -0
- metadata +87 -27
- data/README.rdoc +0 -44
- data/lib/rubyfca/trollop.rb +0 -739
- data/rubyfca.log +0 -0
- data/test/test_helper.rb +0 -10
- /data/test/{test_data.csv → test_data_.csv} +0 -0
data/lib/rubyfca.rb
CHANGED
@@ -1,40 +1,39 @@
|
|
1
|
-
|
2
|
-
# -*- coding: utf-8 -*-
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
3
|
## lib/rubyfca.rb -- Formal Concept Analysis tool in Ruby
|
5
4
|
## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
|
6
|
-
## Kow Kuroda (mailto: kuroda@nict.go.jp)
|
7
|
-
## Copyright:: Copyright 2009 Yoichiro Hasebe and Kow Kuroda
|
5
|
+
## Kow Kuroda (mailto: kuroda@nict.go.jp)
|
6
|
+
## Copyright:: Copyright 2009-2023 Yoichiro Hasebe and Kow Kuroda
|
8
7
|
## License:: GNU GPL version 3
|
9
8
|
|
10
|
-
|
9
|
+
require "csv"
|
10
|
+
require "optimist"
|
11
|
+
require "roo"
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
require 'version'
|
15
|
-
require 'trollop'
|
13
|
+
require_relative "./rubyfca/ruby_graphviz"
|
14
|
+
require_relative "./rubyfca/version"
|
16
15
|
|
17
16
|
private
|
18
17
|
|
19
|
-
## Take two arrays each consisting of 0s and 1s and create their logical disjunction
|
18
|
+
## Take two arrays each consisting of 0s and 1s and create their logical disjunction
|
20
19
|
def create_and_ary(a, b)
|
21
20
|
return false if (s = a.size) != b.size
|
22
|
-
|
23
|
-
|
21
|
+
|
22
|
+
(0..(s - 1)).to_a.map { |i| a[i].to_i & b[i].to_i }
|
24
23
|
end
|
25
24
|
|
26
25
|
## Take two arrays each consisting of 0s and 1s and create their logical conjunction
|
27
26
|
def create_or_ary(a, b)
|
28
27
|
return false if (s = a.size) != b.size
|
29
|
-
|
30
|
-
|
28
|
+
|
29
|
+
(0..(s - 1)).to_a.map { |i| a[i].to_i | b[i].to_i }
|
31
30
|
end
|
32
31
|
|
33
32
|
public
|
34
33
|
|
35
34
|
## Take care of errors
|
36
35
|
def showerror(sentence, severity)
|
37
|
-
if severity
|
36
|
+
if severity.zero?
|
38
37
|
puts "Warning: #{sentence} The output may not be meaningful."
|
39
38
|
elsif severity == 1
|
40
39
|
puts "Error: #{sentence} No output generated."
|
@@ -44,26 +43,27 @@ end
|
|
44
43
|
|
45
44
|
## Basic structure of the code is the same as Fcastone written in Perl by Uta Priss
|
46
45
|
class FormalContext
|
47
|
-
|
48
46
|
## Converte cxt data to three basic structures of objects, attributes, and matrix
|
49
|
-
def initialize(input, mode, label_contraction
|
50
|
-
if input.
|
51
|
-
|
52
|
-
end
|
53
|
-
input.gsub!(" ", " ")
|
47
|
+
def initialize(input, mode, label_contraction: false)
|
48
|
+
showerror("File is empty", 1) if input.empty?
|
49
|
+
input = input.gsub(" ", " ")
|
54
50
|
begin
|
55
51
|
case mode
|
56
52
|
when /cxt\z/
|
57
53
|
read_cxt(input)
|
58
54
|
when /csv\z/
|
59
55
|
read_csv(input)
|
56
|
+
when /xlsx\z/
|
57
|
+
read_xlsx(input)
|
60
58
|
end
|
61
|
-
rescue => e
|
62
|
-
|
59
|
+
rescue StandardError => e
|
60
|
+
pp e.message
|
61
|
+
pp e.backtrace
|
62
|
+
showerror("Input data contains a syntactic problem.", 1)
|
63
63
|
end
|
64
64
|
@label_contraction = label_contraction
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
## process cxt data
|
68
68
|
def read_cxt(input)
|
69
69
|
lines = input.split
|
@@ -71,26 +71,41 @@ class FormalContext
|
|
71
71
|
if (lines[0] !~ /B/i || (2 * lines[1].to_i + lines[2].to_i + t1) != lines.size)
|
72
72
|
showerror("Wrong cxt format!", 1)
|
73
73
|
end
|
74
|
-
@objects
|
75
|
-
@attributes = lines[(lines[1].to_i + t1)
|
76
|
-
lines
|
77
|
-
@matrix
|
74
|
+
@objects = lines[t1..(lines[1].to_i + t1 - 1)]
|
75
|
+
@attributes = lines[(lines[1].to_i + t1)..(lines[1].to_i + lines[2].to_i + t1 - 1)]
|
76
|
+
lines = lines[(lines[1].to_i + lines[2].to_i + t1)..lines.size]
|
77
|
+
@matrix = changecrosssymbol("X", "\\.", lines)
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
# process csv data using the standard csv library
|
81
81
|
def read_csv(input)
|
82
82
|
input = remove_blank(input)
|
83
83
|
data = CSV.parse(input)
|
84
|
-
@objects = trim_ary(data.transpose.first[1
|
85
|
-
@attributes = trim_ary(data.first[1
|
84
|
+
@objects = trim_ary(data.transpose.first[1..])
|
85
|
+
@attributes = trim_ary(data.first[1..])
|
86
|
+
@matrix = []
|
87
|
+
data[1..].each do |line|
|
88
|
+
@matrix << line[1..].collect { |cell| /x/i =~ cell ? 1 : 0 }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# process xlsx data using the roo gem
|
93
|
+
def read_xlsx(file_path)
|
94
|
+
xlsx = Roo::Spreadsheet.open(file_path, extension: :xlsx)
|
95
|
+
@objects = xlsx.sheet(0).column(1)[1..]
|
96
|
+
@attributes = xlsx.sheet(0).row(1)[1..]
|
86
97
|
@matrix = []
|
87
|
-
|
88
|
-
|
98
|
+
(2..xlsx.sheet(0).last_row).each do |row|
|
99
|
+
matrix_row = []
|
100
|
+
(2..xlsx.sheet(0).last_column).each do |col|
|
101
|
+
matrix_row << (xlsx.sheet(0).cell(row, col) =~ /x/i ? 1 : 0)
|
102
|
+
end
|
103
|
+
@matrix << matrix_row
|
89
104
|
end
|
90
105
|
end
|
91
|
-
|
106
|
+
|
92
107
|
def remove_blank(input)
|
93
|
-
blank_removed = ""
|
108
|
+
blank_removed = +""
|
94
109
|
input.split("\n").each do |line|
|
95
110
|
line = line.strip
|
96
111
|
unless /\A\s*\z/ =~ line
|
@@ -99,26 +114,22 @@ class FormalContext
|
|
99
114
|
end
|
100
115
|
blank_removed
|
101
116
|
end
|
102
|
-
|
117
|
+
|
103
118
|
def trim_ary(ary)
|
104
|
-
|
105
|
-
cell.strip
|
106
|
-
end
|
107
|
-
newary
|
119
|
+
ary.map(&:strip)
|
108
120
|
end
|
109
|
-
|
121
|
+
|
110
122
|
## Apply a formal concept analysis on the matrix
|
111
|
-
def calcurate
|
123
|
+
def calcurate
|
112
124
|
@concepts, @extM, @intM = ganter_alg(@matrix)
|
113
125
|
@relM, @reltrans, @rank = create_rel(@intM)
|
114
126
|
@gammaM, @muM = gammaMu(@extM, @intM, @matrix)
|
115
127
|
end
|
116
128
|
|
117
|
-
## This is an implementation of an algorithm described by Bernhard Ganter
|
129
|
+
## This is an implementation of an algorithm described by Bernhard Ganter
|
118
130
|
## in "Two basic algorithms in concept analysis." Technische Hochschule
|
119
131
|
## Darmstadt, FB4-Preprint, 831, 1984.
|
120
132
|
def ganter_alg(matrix)
|
121
|
-
|
122
133
|
m = matrix
|
123
134
|
|
124
135
|
## all arrays except @idx are arrays of arrays of 0's and 1's
|
@@ -129,25 +140,25 @@ class FormalContext
|
|
129
140
|
int_B = []
|
130
141
|
endkey = []
|
131
142
|
temp = []
|
132
|
-
|
143
|
+
|
133
144
|
## the level in the lattice from the top
|
134
|
-
lvl = 0
|
145
|
+
lvl = 0
|
135
146
|
## is lower than the index of leftmost attr
|
136
|
-
idx[lvl] = -1
|
147
|
+
idx[lvl] = -1
|
137
148
|
## number of attr. and objs
|
138
|
-
anzM = m.size
|
139
|
-
## only needed for initialization
|
149
|
+
anzM = m.size
|
150
|
+
## only needed for initialization
|
140
151
|
anzG = m[0].size
|
141
152
|
## initialize extA[0] = [1,...,1]
|
142
|
-
anzG.times do
|
143
|
-
|
153
|
+
anzG.times do
|
154
|
+
unless ext_A[0]
|
144
155
|
ext_A[0] = []
|
145
156
|
end
|
146
157
|
ext_A[0] << 1
|
147
158
|
end
|
148
159
|
## initialize extB[0] = [0,...,0]
|
149
|
-
anzM.times do
|
150
|
-
|
160
|
+
anzM.times do
|
161
|
+
unless int_B[0]
|
151
162
|
int_B[0] = []
|
152
163
|
end
|
153
164
|
int_B[0] << 0
|
@@ -155,7 +166,7 @@ class FormalContext
|
|
155
166
|
anzCpt = 0
|
156
167
|
extension[0] = ext_A[0]
|
157
168
|
intension[0] = int_B[0]
|
158
|
-
anzM.times do
|
169
|
+
anzM.times do
|
159
170
|
endkey << 1
|
160
171
|
end
|
161
172
|
|
@@ -164,13 +175,11 @@ class FormalContext
|
|
164
175
|
(anzM - 1).downto(0) do |i|
|
165
176
|
breakkey = false
|
166
177
|
if (int_B[lvl][i] != 1)
|
167
|
-
while (i < idx[lvl])
|
168
|
-
lvl -= 1
|
169
|
-
end
|
178
|
+
lvl -= 1 while (i < idx[lvl])
|
170
179
|
idx[lvl + 1] = i
|
171
180
|
ext_A[lvl + 1] = create_and_ary(ext_A[lvl], m[i])
|
172
181
|
0.upto(i - 1) do |j|
|
173
|
-
if(!breakkey && int_B[lvl][j] != 1)
|
182
|
+
if (!breakkey && int_B[lvl][j] != 1)
|
174
183
|
temp = create_and_ary(ext_A[lvl + 1], m[j])
|
175
184
|
if temp == ext_A[lvl + 1]
|
176
185
|
breakkey = true
|
@@ -180,7 +189,7 @@ class FormalContext
|
|
180
189
|
unless breakkey
|
181
190
|
int_B[lvl + 1] = int_B[lvl].dup
|
182
191
|
int_B[lvl + 1][i] = 1
|
183
|
-
(i+1).upto(anzM - 1) do |k|
|
192
|
+
(i + 1).upto(anzM - 1) do |k|
|
184
193
|
if int_B[lvl + 1][k] != 1
|
185
194
|
temp = create_and_ary(ext_A[lvl + 1], m[k])
|
186
195
|
if temp == ext_A[lvl + 1]
|
@@ -194,7 +203,7 @@ class FormalContext
|
|
194
203
|
intension[anzCpt] = int_B[lvl]
|
195
204
|
break
|
196
205
|
end
|
197
|
-
end
|
206
|
+
end
|
198
207
|
end
|
199
208
|
end
|
200
209
|
|
@@ -214,7 +223,7 @@ class FormalContext
|
|
214
223
|
[c, intension, extension]
|
215
224
|
end
|
216
225
|
|
217
|
-
## Output arrayconsists of the following:
|
226
|
+
## Output arrayconsists of the following:
|
218
227
|
## r (subconcept superconcept relation)
|
219
228
|
## rt (trans. closure of r)
|
220
229
|
## s (ranked concepts)
|
@@ -231,11 +240,11 @@ class FormalContext
|
|
231
240
|
unless r[i]
|
232
241
|
r[i] = []
|
233
242
|
end
|
234
|
-
r[i][j] = 0
|
243
|
+
r[i][j] = 0
|
235
244
|
unless rt[i]
|
236
245
|
rt[i] = []
|
237
246
|
end
|
238
|
-
rt[i][j] = 0
|
247
|
+
rt[i][j] = 0
|
239
248
|
end
|
240
249
|
end
|
241
250
|
|
@@ -265,16 +274,15 @@ class FormalContext
|
|
265
274
|
s[rank[i]] = []
|
266
275
|
end
|
267
276
|
s[rank[i]] << i
|
268
|
-
end
|
277
|
+
end
|
269
278
|
s = s.collect do |i|
|
270
|
-
i
|
279
|
+
i || [0]
|
271
280
|
end
|
272
281
|
|
273
282
|
[r, rt, s]
|
274
|
-
end
|
283
|
+
end
|
275
284
|
|
276
285
|
def gammaMu(extent, intent, cxt)
|
277
|
-
|
278
286
|
gamma = []
|
279
287
|
mu = []
|
280
288
|
invcxt = []
|
@@ -288,31 +296,31 @@ class FormalContext
|
|
288
296
|
0.upto(intent.size - 1) do |j|
|
289
297
|
0.upto(cxt.size - 1) do |i|
|
290
298
|
gamma[i] = [] unless gamma[i]
|
291
|
-
if cxt[i] == intent[j]
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
299
|
+
gamma[i][j] = if cxt[i] == intent[j]
|
300
|
+
2
|
301
|
+
elsif (!@label_contraction && create_or_ary(cxt[i], intent[j]) == cxt[i])
|
302
|
+
1
|
303
|
+
else
|
304
|
+
0
|
305
|
+
end
|
298
306
|
end
|
299
|
-
|
307
|
+
|
300
308
|
0.upto(invcxt.size - 1) do |i|
|
301
309
|
# next unless invcxt[i]
|
302
310
|
mu[i] = [] unless mu[i]
|
303
|
-
if invcxt[i] == extent[j]
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
311
|
+
mu[i][j] = if invcxt[i] == extent[j]
|
312
|
+
2
|
313
|
+
elsif (!@label_contraction && create_or_ary(invcxt[i], extent[j]) == invcxt[i])
|
314
|
+
1
|
315
|
+
else
|
316
|
+
0
|
317
|
+
end
|
310
318
|
end
|
311
319
|
end
|
312
|
-
|
320
|
+
|
313
321
|
[gamma, mu]
|
314
322
|
end
|
315
|
-
|
323
|
+
|
316
324
|
def changecrosssymbol(char1, char2, lns)
|
317
325
|
rel = []
|
318
326
|
lns.each do |ln|
|
@@ -333,36 +341,36 @@ class FormalContext
|
|
333
341
|
## Generate Graphviz dot data (not creating a file)
|
334
342
|
## For options, see 'rubyfca'
|
335
343
|
def generate_dot(opts)
|
336
|
-
index_max_width = @concepts.size.to_s.split(//).size
|
337
|
-
|
338
|
-
|
339
|
-
clattice = RubyGraphviz.new("clattice", :
|
340
|
-
|
341
|
-
if opts[:circle]
|
342
|
-
legend
|
343
|
-
legend.node_default(:
|
344
|
-
legend.edge_default(:
|
344
|
+
# index_max_width = @concepts.size.to_s.split(//).size
|
345
|
+
nodesep = opts[:nodesep] ? opts[:nodesep].to_s : "0.4"
|
346
|
+
ranksep = opts[:ranksep] ? opts[:ranksep].to_s : "0.2"
|
347
|
+
clattice = RubyGraphviz.new("clattice", rankdir: "", nodesep: nodesep, ranksep: ranksep)
|
348
|
+
|
349
|
+
if opts[:circle] && opts[:legend]
|
350
|
+
legend = RubyGraphviz.new("legend", rankdir: "TB", lebelloc: "t", centered: "false")
|
351
|
+
legend.node_default(shape: "plaintext")
|
352
|
+
legend.edge_default(color: "gray60") if opts[:coloring]
|
345
353
|
legends = []
|
346
354
|
end
|
347
355
|
|
348
356
|
if opts[:circle]
|
349
|
-
clattice.node_default(:
|
350
|
-
clattice.edge_default(:
|
351
|
-
clattice.edge_default(:
|
357
|
+
clattice.node_default(shape: "circle", style: "filled")
|
358
|
+
clattice.edge_default(dir: "none", minlen: "2")
|
359
|
+
clattice.edge_default(color: "gray60") if opts[:coloring]
|
352
360
|
else
|
353
|
-
clattice.node_default(:
|
354
|
-
clattice.edge_default(:
|
355
|
-
clattice.edge_default(:
|
361
|
+
clattice.node_default(shape: "record", margin: "0.2,0.055")
|
362
|
+
clattice.edge_default(dir: "none")
|
363
|
+
clattice.edge_default(color: "gray60") if opts[:coloring]
|
356
364
|
end
|
357
|
-
|
365
|
+
|
358
366
|
0.upto(@concepts.size - 1) do |i|
|
359
367
|
objfull = []
|
360
|
-
attrfull = []
|
368
|
+
attrfull = []
|
361
369
|
0.upto(@gammaM.size - 1) do |j|
|
362
370
|
if @gammaM[j][i] == 2
|
363
|
-
|
371
|
+
# pointing finger does not appear correctly in eps...
|
364
372
|
# obj = opts[:full] ? @objects[j] + " " + [0x261C].pack("U") : @objects[j]
|
365
|
-
obj = opts[:full] ? @objects[j] + "*" : @objects[j]
|
373
|
+
obj = opts[:full] ? @objects[j].to_s + "*" : @objects[j].to_s
|
366
374
|
objfull << obj
|
367
375
|
elsif @gammaM[j][i] == 1
|
368
376
|
objfull << @objects[j]
|
@@ -370,9 +378,9 @@ class FormalContext
|
|
370
378
|
end
|
371
379
|
0.upto(@muM.size - 1) do |k|
|
372
380
|
if @muM[k][i] == 2
|
373
|
-
|
381
|
+
# pointing finger does not appear correctly in eps...
|
374
382
|
# att = opts[:full] ? @attributes[k] + " " + [0x261C].pack("U") : @attributes[k]
|
375
|
-
att = opts[:full] ? @attributes[k] + "*" : @attributes[k]
|
383
|
+
att = opts[:full] ? @attributes[k].to_s + "*" : @attributes[k].to_s
|
376
384
|
attrfull << att
|
377
385
|
elsif @muM[k][i] == 1
|
378
386
|
attrfull << @attributes[k]
|
@@ -381,21 +389,21 @@ class FormalContext
|
|
381
389
|
|
382
390
|
concept_id = i + 1
|
383
391
|
|
384
|
-
attr_str = attrfull.join(
|
392
|
+
attr_str = attrfull.join("<br />")
|
385
393
|
attr_str = attr_str == "" ? " " : attr_str
|
386
|
-
|
387
|
-
if opts[:coloring]
|
394
|
+
|
395
|
+
if opts[:coloring].zero? || /\A\s+\z/ =~ attr_str
|
388
396
|
attr_color = "white"
|
389
397
|
elsif opts[:coloring] == 1
|
390
398
|
attr_color = "lightblue"
|
391
399
|
elsif opts[:coloring] == 2
|
392
400
|
attr_color = "gray87"
|
393
401
|
end
|
394
|
-
|
395
|
-
obj_str = objfull.join(
|
402
|
+
|
403
|
+
obj_str = objfull.join("<br />")
|
396
404
|
obj_str = obj_str == "" ? " " : obj_str
|
397
405
|
|
398
|
-
if opts[:coloring]
|
406
|
+
if opts[:coloring].zero? || /\A\s+\z/ =~ obj_str
|
399
407
|
obj_color = "white"
|
400
408
|
elsif opts[:coloring] == 1
|
401
409
|
obj_color = "pink"
|
@@ -403,37 +411,36 @@ class FormalContext
|
|
403
411
|
obj_color = "gray92"
|
404
412
|
end
|
405
413
|
|
406
|
-
label = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">"
|
407
|
-
|
408
|
-
|
409
|
-
|
414
|
+
label = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" \
|
415
|
+
"<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" \
|
416
|
+
"<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" \
|
417
|
+
"</table>>"
|
410
418
|
|
411
|
-
if opts[:circle]
|
419
|
+
if opts[:circle] && opts[:legend]
|
412
420
|
|
413
|
-
leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">"
|
414
|
-
|
415
|
-
|
416
|
-
|
421
|
+
leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" \
|
422
|
+
"<tr><td rowspan=\"2\">#{concept_id}</td><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" \
|
423
|
+
"<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" \
|
424
|
+
"</table>>"
|
417
425
|
|
418
|
-
if !attrfull.empty?
|
419
|
-
legend.node("cl#{concept_id}k", :
|
420
|
-
legend.node("cl#{concept_id}v", :
|
421
|
-
legend.rank("cl#{concept_id}k", "cl#{concept_id}v", :
|
426
|
+
if !attrfull.empty? || !objfull.empty?
|
427
|
+
legend.node("cl#{concept_id}k", label: concept_id, style: "invis")
|
428
|
+
legend.node("cl#{concept_id}v", label: leg, fillcolor: "white")
|
429
|
+
legend.rank("cl#{concept_id}k", "cl#{concept_id}v", style: "invis", length: "0.0")
|
422
430
|
if legends[-1]
|
423
|
-
legend.edge("cl#{legends[-1]}k", "cl#{concept_id}k", :
|
431
|
+
legend.edge("cl#{legends[-1]}k", "cl#{concept_id}k", style: "invis", length: "0.0")
|
424
432
|
end
|
425
433
|
legends << concept_id
|
426
434
|
end
|
427
435
|
end
|
428
|
-
|
436
|
+
|
429
437
|
if opts[:circle]
|
430
|
-
clattice.node("c#{i}", :
|
438
|
+
clattice.node("c#{i}", width: "0.5", fontsize: "14.0", label: concept_id)
|
431
439
|
else
|
432
|
-
clattice.node("c#{i}", :label
|
433
|
-
:height => "0.0", :width => "0.0", :margin => "0.0")
|
440
|
+
clattice.node("c#{i}", label: label, shape: "plaintext", height: "0.0", width: "0.0", margin: "0.0")
|
434
441
|
end
|
435
442
|
end
|
436
|
-
|
443
|
+
|
437
444
|
0.upto(@relM.size - 1) do |i|
|
438
445
|
0.upto(@relM.size - 1) do |j|
|
439
446
|
if @relM[i][j] == 1
|
@@ -441,11 +448,11 @@ class FormalContext
|
|
441
448
|
end
|
442
449
|
end
|
443
450
|
end
|
444
|
-
|
445
|
-
clattice.subgraph(legend) if opts[:circle]
|
451
|
+
|
452
|
+
clattice.subgraph(legend) if opts[:circle] && opts[:legend]
|
446
453
|
clattice.to_dot
|
447
454
|
end
|
448
|
-
|
455
|
+
|
449
456
|
## Generate an actual graphic file (Graphviz dot needs to be installed properly)
|
450
457
|
def generate_img(outfile, image_type, opts)
|
451
458
|
dot = generate_dot(opts)
|
@@ -453,15 +460,14 @@ class FormalContext
|
|
453
460
|
if isthere_dot !~ /dot.*version/i
|
454
461
|
showerror("Graphviz's dot program cannot be found.", 1)
|
455
462
|
else
|
456
|
-
if opts[:straight]
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
IO.popen(cmd,
|
463
|
+
cmd = if opts[:straight]
|
464
|
+
"dot | neato -n -T#{image_type} -o#{outfile} 2>rubyfca.log"
|
465
|
+
else
|
466
|
+
"dot -T#{image_type} -o#{outfile} 2>rubyfca.log"
|
467
|
+
end
|
468
|
+
IO.popen(cmd, "r+") do |io|
|
462
469
|
io.puts dot
|
463
470
|
end
|
464
471
|
end
|
465
|
-
end
|
472
|
+
end
|
466
473
|
end
|
467
|
-
|
data/rubyfca.gemspec
CHANGED
@@ -1,17 +1,25 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("lib/rubyfca/version", __dir__)
|
3
4
|
|
4
5
|
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "rubyfca"
|
7
|
+
gem.version = RubyFCA::VERSION
|
5
8
|
gem.authors = ["Yoichiro Hasebe", "Kow Kuroda"]
|
6
9
|
gem.email = ["yohasebe@gmail.com"]
|
7
|
-
gem.summary
|
8
|
-
gem.description
|
9
|
-
gem.homepage
|
10
|
+
gem.summary = "Command line FCA tool written in Ruby"
|
11
|
+
gem.description = "Command line Formal Concept Analysis (FCA) tool written in Ruby"
|
12
|
+
gem.homepage = "http://github.com/yohasebe/rubyfca"
|
10
13
|
|
11
|
-
gem.
|
12
|
-
gem.
|
13
|
-
gem.
|
14
|
-
gem.
|
14
|
+
gem.required_ruby_version = ">= 2.6.10"
|
15
|
+
gem.licenses = ["GPL-3.0"]
|
16
|
+
gem.files = `git ls-files`.split("\n")
|
17
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
15
18
|
gem.require_paths = ["lib"]
|
16
|
-
|
19
|
+
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
|
22
|
+
gem.add_dependency "optimist"
|
23
|
+
gem.add_dependency "roo"
|
24
|
+
gem.add_development_dependency "minitest"
|
17
25
|
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|