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/README.md
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# 💭 RubyFCA
|
2
|
+
|
3
|
+
Command line tool for Formal Concept Analysis (FCA) written in Ruby.
|
4
|
+
|
5
|
+
<kbd>
|
6
|
+
<img src="./samples/fca-source-result.png" width="800px"/>
|
7
|
+
</kbd>
|
8
|
+
|
9
|
+
|
10
|
+
## Change Log
|
11
|
+
|
12
|
+
- Microsoft Excel `.xlsx` files supported [June 18, 2023]
|
13
|
+
- Sample files added [June 18, 2023]
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
17
|
+
* Converts data in XLSX (Microsoft Excel), CSV (comma-separated values), or CXT ([Conexp](https://github.com/fcatools) and generate a Graphviz DOT file, a SVG/EPS vector image file, or /a PNG/JPG bitmap file.
|
18
|
+
* Adopts the Ganter algorithm (with reference to its Perl implementation of [Fcastone](https://upriss.github.io/fcastone) by Uta Priss).
|
19
|
+
|
20
|
+
## Dependencies
|
21
|
+
|
22
|
+
- [Graphviz](https://graphviz.org/)
|
23
|
+
|
24
|
+
For example, to install Graphviz using Homebrew on MacOS, execute the following command
|
25
|
+
|
26
|
+
brew install graphviz
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Install the gem:
|
31
|
+
|
32
|
+
gem install rubyfca
|
33
|
+
|
34
|
+
## How to Use
|
35
|
+
|
36
|
+
RubuFCA converts Conexp CXT data to Graphviz dot format.
|
37
|
+
|
38
|
+
Usage:
|
39
|
+
rubyfca [options] <source file> <output file>
|
40
|
+
|
41
|
+
where:
|
42
|
+
<source file>
|
43
|
+
".xlsx", ".csv" ,".cxt"
|
44
|
+
<output file>
|
45
|
+
."svg", ".png", ".jpg", ".eps", or ".dot"
|
46
|
+
[options]:
|
47
|
+
--full, -f: Do not contract concept labels
|
48
|
+
--coloring, -c <i>: Color concept nodes [0 = none (default), 1 =
|
49
|
+
lightblue/pink, 2 = monochrome] (default: 0)
|
50
|
+
--straight, -s: Straighten edges (available when output format is
|
51
|
+
either png, jpg, svg, pdf, or eps)
|
52
|
+
--nodesep, -n <f>: Size of separation between sister nodes (from 0.1 to
|
53
|
+
5.0) (default: 0.4)
|
54
|
+
--ranksep, -r <f>: Size of separation between ranks (from 0.1 to 5.0)
|
55
|
+
(default: 0.2)
|
56
|
+
--legend, -l: Print the legend of concept nodes (available only when
|
57
|
+
using circle node shape)
|
58
|
+
--circle, -i: Use circle shaped concept nodes
|
59
|
+
--version, -v: Print version and exit
|
60
|
+
--help, -h: Show this message
|
61
|
+
|
62
|
+
## Examples
|
63
|
+
|
64
|
+
### Input Data
|
65
|
+
|
66
|
+
#### XLSX (Excel)
|
67
|
+
|
68
|
+
<kbd>
|
69
|
+
<img src="./samples/xlsx-sample.png" width="800px"/>
|
70
|
+
</kbd>
|
71
|
+
|
72
|
+
#### CSV
|
73
|
+
|
74
|
+
```
|
75
|
+
, Ostrich , Sparrow , Eagle , Lion , Bonobo , Human being
|
76
|
+
bird , X , X , X , , ,
|
77
|
+
mammal , , , , X , X , X
|
78
|
+
ape , , , , , X , X
|
79
|
+
flying , , X , X , , ,
|
80
|
+
preying , , , X , X , ,
|
81
|
+
talking , , , , , , X
|
82
|
+
```
|
83
|
+
|
84
|
+
#### CXT
|
85
|
+
|
86
|
+
```
|
87
|
+
B
|
88
|
+
|
89
|
+
6
|
90
|
+
6
|
91
|
+
|
92
|
+
Ostrich
|
93
|
+
Sparrow
|
94
|
+
Eagle
|
95
|
+
Lion
|
96
|
+
Bonobo
|
97
|
+
Human being
|
98
|
+
bird
|
99
|
+
mammal
|
100
|
+
ape
|
101
|
+
flying
|
102
|
+
preying
|
103
|
+
talking
|
104
|
+
XXX...
|
105
|
+
...XXX
|
106
|
+
....XX
|
107
|
+
.XX...
|
108
|
+
..XX..
|
109
|
+
.....X
|
110
|
+
```
|
111
|
+
|
112
|
+
## Output
|
113
|
+
|
114
|
+
`rubyfca input_file output_file --coloring 1 --full --nodesep 0.8 --ranksep 0.3 --straight`
|
115
|
+
|
116
|
+
<img src="./samples/sample_01.svg" width="500px"/>
|
117
|
+
|
118
|
+
|
119
|
+
`rubyfca input_file output_file --coloring 2 --nodesep 0.5 --ranksep 0.3`
|
120
|
+
|
121
|
+
<img src="./samples/sample_02.svg" width="400px"/>
|
122
|
+
|
123
|
+
`rubyfca input_file output_file --circle --legend --coloring 1 --full --nodesep 0.8 --ranksep 0.3 --straight`
|
124
|
+
|
125
|
+
<img src="./samples/sample_03.svg" width="800px"/>
|
126
|
+
|
127
|
+
## Copyright
|
128
|
+
|
129
|
+
Copyright (c) 2009-2023 Yoichiro Hasebe and Kow Kuroda. See LICENSE for details.
|
data/Rakefile
CHANGED
data/bin/rubyfca
CHANGED
@@ -1,86 +1,92 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
Encoding.default_external = "UTF-8"
|
6
|
+
|
7
|
+
require_relative "../lib/rubyfca"
|
6
8
|
|
7
9
|
################ parse options ##########
|
8
10
|
|
9
|
-
opts =
|
11
|
+
opts = Optimist.options do
|
10
12
|
version RubyFCA::VERSION
|
11
|
-
banner
|
12
|
-
|
13
|
-
RubuFCA converts Conexp CXT data to Graphviz dot format.
|
14
|
-
|
15
|
-
Usage:
|
16
|
-
rubyfca [options] <source file> <output file>
|
17
|
-
|
18
|
-
where:
|
19
|
-
<source file>
|
20
|
-
".cxt", ".csv"
|
21
|
-
<output file>
|
22
|
-
."dot", ".png", ".jpg", or ".eps"
|
23
|
-
[options]:
|
24
|
-
EOS
|
25
|
-
|
26
|
-
opt :full, "Do not contract concept labels", :default=> false
|
27
|
-
opt :coloring, "Color concept nodes [0 = none (default), 1 = lightblue/pink, 2 = monochrome]", :default => 0
|
28
|
-
opt :straight, "Straighten edges (available when output format is either png, jpg, svg, pdf, or eps)", :default => false
|
29
|
-
opt :nodesep, "Size of separation between sister nodes (from 0.1 to 5.0)", :default => 0.4
|
30
|
-
opt :ranksep, "Size of separation between ranks (from 0.1 to 5.0)", :default => 0.2
|
31
|
-
opt :legend, "Print the legend of concept nodes (available only when using circle node shape)", :default => false
|
32
|
-
opt :circle, "Use circle shaped concept nodes", :default=> false
|
13
|
+
banner <<~USAGE
|
14
|
+
RubuFCA converts Conexp CXT data to Graphviz dot format.
|
33
15
|
|
34
|
-
|
35
|
-
|
36
|
-
Trollop::die :ranksep, "must be within 0.1 - 5.0" if (opts[:ranksep] < 0.1 || opts[:ranksep] > 5.0)
|
37
|
-
Trollop::die :nodesep, "must be within 0.1 - 5.0" if (opts[:nodesep] < 0.1 || opts[:nodesep] > 5.0)
|
38
|
-
############### main program ###############
|
16
|
+
Usage:
|
17
|
+
rubyfca [options] <source file> <output file>
|
39
18
|
|
40
|
-
|
41
|
-
|
19
|
+
where:
|
20
|
+
<source file>
|
21
|
+
".xlsx", ".csv", ".cxt"
|
22
|
+
<output file>
|
23
|
+
".svg", ".png", ".jpg", ".eps", or ."dot"
|
24
|
+
[options]:
|
25
|
+
USAGE
|
26
|
+
|
27
|
+
opt :full, "Do not contract concept labels", default: false
|
28
|
+
opt :coloring, "Color concept nodes [0 = none (default), 1 = lightblue/pink, 2 = monochrome]", default: 0
|
29
|
+
opt :straight, "Straighten edges (available when output format is either png, jpg, svg, pdf, or eps)", default: false
|
30
|
+
opt :nodesep, "Size of separation between sister nodes (from 0.1 to 5.0)", default: 0.4
|
31
|
+
opt :ranksep, "Size of separation between ranks (from 0.1 to 5.0)", default: 0.2
|
32
|
+
opt :legend, "Print the legend of concept nodes (available only when using circle node shape)", default: false
|
33
|
+
opt :circle, "Use circle shaped concept nodes", default: false
|
42
34
|
end
|
43
35
|
|
44
|
-
|
45
|
-
|
36
|
+
Trollop.die :coloring, "must be 0, 1, or 2" if opts[:coloring] > 2 || opts[:coloring].negative?
|
37
|
+
Trollop.die :ranksep, "must be within 0.1 - 5.0" if opts[:ranksep] < 0.1 || opts[:ranksep] > 5.0
|
38
|
+
Trollop.die :nodesep, "must be within 0.1 - 5.0" if opts[:nodesep] < 0.1 || opts[:nodesep] > 5.0
|
39
|
+
|
40
|
+
############### main program ###############
|
41
|
+
|
42
|
+
ARGV.size != 2 && showerror("Input and output files are not set properly", 1)
|
43
|
+
|
44
|
+
filename1 = ARGV[0] # input filename
|
45
|
+
filename2 = ARGV[1] # output filename
|
46
46
|
|
47
47
|
#
|
48
48
|
# extract input and output file types
|
49
49
|
#
|
50
|
-
input_type = filename1.slice(/\.[
|
51
|
-
output_type = filename2.slice(/\.[
|
50
|
+
input_type = filename1.slice(/\.[^.]+\z/).split(//)[1..].join("")
|
51
|
+
output_type = filename2.slice(/\.[^.]+\z/).split(//)[1..].join("")
|
52
52
|
|
53
|
-
if (input_type !~ /\A(cxt|csv)\z/ ||
|
53
|
+
if (input_type !~ /\A(cxt|csv|xlsx)\z/ ||
|
54
|
+
output_type !~ /\A(dot|png|jpg|svg|pdf|eps)\z/)
|
54
55
|
showerror("These file extensions are not (yet) supported.", 1)
|
55
56
|
end
|
56
57
|
|
57
58
|
#
|
58
|
-
# input data is kept as plain text
|
59
|
+
# input data is kept as plain text unless file type is "xlsx"
|
59
60
|
#
|
60
|
-
|
61
|
-
|
62
|
-
|
61
|
+
|
62
|
+
if input_type == "xlsx"
|
63
|
+
inputdata = filename1
|
64
|
+
else
|
65
|
+
f = File.open(filename1, "r:UTF-8:UTF-8")
|
66
|
+
inputdata = f.read
|
67
|
+
inputdata.gsub!(/\r\n?/) { "\n" }
|
68
|
+
f.close
|
69
|
+
end
|
63
70
|
|
64
71
|
#
|
65
72
|
# ask for confirmation of overwriting an exisiting file
|
66
73
|
#
|
67
|
-
if
|
74
|
+
if File.exist?(filename2) && !opts[:sil]
|
68
75
|
print "#{filename2} exists and will be overwritten, OK? [y/n]"
|
69
|
-
var1 =
|
70
|
-
if /y/i !~ var1
|
71
|
-
exit;
|
72
|
-
end
|
76
|
+
var1 = $stdin.gets
|
77
|
+
exit if /y/i !~ var1
|
73
78
|
end
|
74
79
|
|
75
80
|
#
|
76
81
|
# context data is converted to a hash table
|
77
82
|
#
|
78
83
|
begin
|
79
|
-
ctxt = FormalContext.new(inputdata, input_type, !opts[:full])
|
84
|
+
ctxt = FormalContext.new(inputdata, input_type, label_contraction: !opts[:full])
|
80
85
|
ctxt.calcurate
|
81
|
-
|
82
|
-
|
83
|
-
|
86
|
+
rescue StandardError => e
|
87
|
+
pp e.message
|
88
|
+
pp e.backtrace
|
89
|
+
showerror("Source data may have problems. Process aborted.", 1)
|
84
90
|
end
|
85
91
|
|
86
92
|
#
|
@@ -88,8 +94,8 @@ end
|
|
88
94
|
#
|
89
95
|
case output_type
|
90
96
|
when "dot"
|
91
|
-
File.open(filename2, "w") do |
|
92
|
-
|
97
|
+
File.open(filename2, "w") do |file|
|
98
|
+
file.write(ctxt.generate_dot(opts))
|
93
99
|
end
|
94
100
|
when "png"
|
95
101
|
ctxt.generate_img(filename2, "png", opts)
|
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
# -*- coding: utf-8 -*-
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
3
|
## lib/ruby_graphviz.rb -- graphviz dot generator library
|
5
4
|
## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
|
@@ -7,8 +6,7 @@
|
|
7
6
|
## License:: GNU GPL version 3
|
8
7
|
|
9
8
|
class RubyGraphviz
|
10
|
-
|
11
|
-
## Example:
|
9
|
+
## Example:
|
12
10
|
##
|
13
11
|
## g = RubyGraphviz.new("newgraph", {:rankdir => "LR", :nodesep => "0.4", :ranksep => "0.2"})
|
14
12
|
##
|
@@ -17,7 +15,7 @@ class RubyGraphviz
|
|
17
15
|
@graph_data = graph_hash
|
18
16
|
@nodes = []
|
19
17
|
@edges = []
|
20
|
-
@dot = ""
|
18
|
+
@dot = +""
|
21
19
|
create_graph
|
22
20
|
end
|
23
21
|
|
@@ -36,31 +34,31 @@ class RubyGraphviz
|
|
36
34
|
end
|
37
35
|
@dot << "]"
|
38
36
|
end
|
39
|
-
@dot << ";\n"
|
37
|
+
@dot << ";\n"
|
40
38
|
end
|
41
|
-
|
39
|
+
|
42
40
|
def finish_graph
|
43
41
|
@dot << "}\n"
|
44
42
|
end
|
45
43
|
|
46
44
|
def create_edge(edgetype, nid1, nid2, edge_hash = nil)
|
47
|
-
temp = " #{nid1
|
45
|
+
temp = " #{nid1} #{edgetype} #{nid2}"
|
48
46
|
index = 0
|
49
47
|
if edge_hash
|
50
|
-
temp << " ["
|
48
|
+
temp << " ["
|
51
49
|
edge_hash.each do |k, v|
|
52
50
|
k = k.to_s
|
53
51
|
temp << "#{k} = \"#{v}\""
|
54
52
|
index += 1
|
55
53
|
temp << ", " unless index == edge_hash.size
|
56
54
|
end
|
57
|
-
temp << "]"
|
55
|
+
temp << "]"
|
58
56
|
end
|
59
|
-
|
57
|
+
temp
|
60
58
|
end
|
61
|
-
|
59
|
+
|
62
60
|
public
|
63
|
-
|
61
|
+
|
64
62
|
## Add a subgraph to a graph (recursively)
|
65
63
|
##
|
66
64
|
## Example:
|
@@ -70,7 +68,7 @@ class RubyGraphviz
|
|
70
68
|
def subgraph(graph)
|
71
69
|
@dot << graph.to_dot.sub(/\Agraph/, "subgraph")
|
72
70
|
end
|
73
|
-
|
71
|
+
|
74
72
|
## Set default options for nodes
|
75
73
|
##
|
76
74
|
## Example:
|
@@ -108,7 +106,7 @@ class RubyGraphviz
|
|
108
106
|
@dot << "];\n"
|
109
107
|
self
|
110
108
|
end
|
111
|
-
|
109
|
+
|
112
110
|
## Create a node with its options
|
113
111
|
##
|
114
112
|
## Example:
|
@@ -116,7 +114,7 @@ class RubyGraphviz
|
|
116
114
|
## graph.node("node-01", :label => "Node 01", :fillcolor => "pink")
|
117
115
|
##
|
118
116
|
def node(node_id, node_hash = nil)
|
119
|
-
@dot << " #{node_id
|
117
|
+
@dot << " #{node_id}"
|
120
118
|
index = 0
|
121
119
|
if node_hash
|
122
120
|
@dot << " ["
|
@@ -132,22 +130,22 @@ class RubyGraphviz
|
|
132
130
|
self
|
133
131
|
end
|
134
132
|
|
135
|
-
## Create a non-directional edge (connection line between nodes) with its options
|
133
|
+
## Create a non-directional edge (connection line between nodes) with its options
|
136
134
|
##
|
137
135
|
## Example:
|
138
136
|
##
|
139
137
|
## graph.edge("node-01", "node-02", :label => "connecting 1 and 2", :color => "lightblue")
|
140
|
-
##
|
138
|
+
##
|
141
139
|
def edge(nid1, nid2, edge_hash = nil)
|
142
140
|
@dot << create_edge("--", nid1, nid2, edge_hash) + ";\n"
|
143
141
|
self
|
144
142
|
end
|
145
143
|
|
146
|
-
## Create a directional edge (arrow from node to node) with its options
|
144
|
+
## Create a directional edge (arrow from node to node) with its options
|
147
145
|
##
|
148
146
|
## Example:
|
149
147
|
## graph.arrow_edge("node-01", "node-02", :label => "from 1 to 2", :color => "lightblue")
|
150
|
-
##
|
148
|
+
##
|
151
149
|
def arrow_edge(nid1, nid2, edge_hash = nil)
|
152
150
|
@dot << create_edge("->", nid1, nid2, edge_hash) + ";\n"
|
153
151
|
self
|
@@ -159,12 +157,11 @@ class RubyGraphviz
|
|
159
157
|
@dot << "{rank=same " + create_edge("--", nid1, nid2, edge_hash) + "}\n"
|
160
158
|
self
|
161
159
|
end
|
162
|
-
|
160
|
+
|
163
161
|
## Convert graph into dot formatted data
|
164
162
|
##
|
165
163
|
def to_dot
|
166
164
|
finish_graph
|
167
|
-
@dot
|
168
|
-
return @dot
|
165
|
+
@dot.gsub(/"</m, "<").gsub(/>"/m, ">")
|
169
166
|
end
|
170
167
|
end
|
data/lib/rubyfca/version.rb
CHANGED