rubyfca 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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