rubyfca 0.2.11 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rubyfca.rb CHANGED
@@ -1,40 +1,39 @@
1
- #!/usr/bin/env ruby
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
- $: << File.dirname(__FILE__) + "/rubyfca"
9
+ require "csv"
10
+ require "optimist"
11
+ require "roo"
11
12
 
12
- require 'csv'
13
- require 'ruby_graphviz'
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
- result = (0..(s-1)).to_a.map{|i| a[i].to_i & b[i].to_i}
23
- return result
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
- result = (0..(s-1)).to_a.map{|i| a[i].to_i | b[i].to_i}
30
- return result
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 == 0
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 = false)
50
- if input.size == 0
51
- showerror("File is empty", 1)
52
- end
53
- input.gsub!(" ", "&nbsp;")
47
+ def initialize(input, mode, label_contraction: false)
48
+ showerror("File is empty", 1) if input.empty?
49
+ input = input.gsub(" ", "&nbsp;")
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
- showerror("Input data contains a syntax problem.", 1)
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 = 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)
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..-1])
85
- @attributes = trim_ary(data.first[1..-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
- data[1..-1].each do |line|
88
- @matrix << line[1..-1].collect { |cell| /x/i =~ cell ? 1 : 0 }
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
- newary = ary.collect do |cell|
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 |i|
143
- if !ext_A[0]
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 |i|
150
- if !int_B[0]
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 |i|
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 ? i : [0]
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
- gamma[i][j] = 2
293
- elsif (!@label_contraction && create_or_ary(cxt[i], intent[j]) == cxt[i])
294
- gamma[i][j] = 1
295
- else
296
- gamma[i][j] = 0
297
- end
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
- mu[i][j] = 2
305
- elsif (!@label_contraction && create_or_ary(invcxt[i], extent[j]) == invcxt[i])
306
- mu[i][j] = 1
307
- else
308
- mu[i][j] = 0
309
- end
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
- nodesep = opts[:nodesep] ? opts[:nodesep].to_s : "0.4"
338
- ranksep = opts[:ranksep] ? opts[:ranksep].to_s : "0.2"
339
- clattice = RubyGraphviz.new("clattice", :rankdir => "", :nodesep => nodesep, :ranksep => ranksep)
340
-
341
- if opts[:circle] and opts[:legend]
342
- legend = RubyGraphviz.new("legend", :rankdir => "TB", :lebelloc => "t", :centered => "false")
343
- legend.node_default(:shape => "plaintext")
344
- legend.edge_default(:color => "gray60") if opts[:coloring]
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(:shape => "circle", :style => "filled")
350
- clattice.edge_default(:dir => "none", :minlen => "2")
351
- clattice.edge_default(:color => "gray60") if opts[:coloring]
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(:shape => "record", :margin => "0.2,0.055")
354
- clattice.edge_default(:dir => "none")
355
- clattice.edge_default(:color => "gray60") if opts[:coloring]
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
- # pointing finger does not appear correctly in eps...
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
- # pointing finger does not appear correctly in eps...
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('<br />')
392
+ attr_str = attrfull.join("<br />")
385
393
  attr_str = attr_str == "" ? " " : attr_str
386
-
387
- if opts[:coloring] == 0 or /\A\s+\z/ =~ attr_str
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('<br />')
402
+
403
+ obj_str = objfull.join("<br />")
396
404
  obj_str = obj_str == "" ? " " : obj_str
397
405
 
398
- if opts[:coloring] == 0 or /\A\s+\z/ =~ obj_str
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
- "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" +
408
- "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" +
409
- "</table>>"
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] and opts[:legend]
419
+ if opts[:circle] && opts[:legend]
412
420
 
413
- leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
414
- "<tr><td rowspan=\"2\">#{concept_id}</td><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" +
415
- "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" +
416
- "</table>>"
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? or !objfull.empty?
419
- legend.node("cl#{concept_id}k", :label => concept_id, :style => "invis")
420
- legend.node("cl#{concept_id}v", :label => leg, :fillcolor => "white")
421
- legend.rank("cl#{concept_id}k", "cl#{concept_id}v", :style => "invis", :length => "0.0")
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", :style => "invis", :length => "0.0")
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}", :width => "0.5", :fontsize => "14.0", :label => concept_id)
438
+ clattice.node("c#{i}", width: "0.5", fontsize: "14.0", label: concept_id)
431
439
  else
432
- clattice.node("c#{i}", :label => label, :shape => "plaintext",
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] and opts[:legend]
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
- cmd = "dot | neato -n -T#{image_type} -o#{outfile} 2>rubyfca.log"
458
- else
459
- cmd = "dot -T#{image_type} -o#{outfile} 2>rubyfca.log"
460
- end
461
- IO.popen(cmd, 'r+') do |io|
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
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/rubyfca/version', __FILE__)
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 = %q{Command line FCA tool written in Ruby}
8
- gem.description = %q{Command line Formal Concept Analysis (FCA) tool written in Ruby}
9
- gem.homepage = "http://github.com/yohasebe/rubyfca"
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.files = `git ls-files`.split($\)
12
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
- gem.name = "rubyfca"
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
- gem.version = RubyFCA::VERSION
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