rubyfca 0.2.10 → 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 +24 -3
- data/Gemfile +4 -0
- data/LICENSE +674 -20
- data/README.md +129 -0
- data/Rakefile +8 -51
- data/bin/rubyfca +61 -57
- data/lib/{ruby_graphviz.rb → rubyfca/ruby_graphviz.rb} +22 -22
- data/lib/rubyfca/version.rb +5 -0
- data/lib/rubyfca.rb +152 -139
- data/rubyfca.gemspec +20 -55
- 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_data_.csv +7 -0
- 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} +5 -5
- 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 +107 -58
- data/README.rdoc +0 -45
- data/VERSION +0 -1
- data/lib/trollop.rb +0 -739
- data/test/test_helper.rb +0 -10
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
@@ -1,56 +1,13 @@
|
|
1
|
-
|
2
|
-
require 'rake'
|
1
|
+
#!/usr/bin/env rake
|
3
2
|
|
4
|
-
|
5
|
-
require 'jeweler'
|
6
|
-
Jeweler::Tasks.new do |gem|
|
7
|
-
gem.name = "rubyfca"
|
8
|
-
gem.summary = %Q{Command line Formal Concept Ananlysis (FCA) tool written in Ruby}
|
9
|
-
gem.description = %Q{Command line Formal Concept Ananlysis (FCA) tool written in Ruby}
|
10
|
-
gem.email = "yohasebe@gmail.com"
|
11
|
-
gem.homepage = "http://github.com/yohasebe/rubyfca"
|
12
|
-
gem.authors = ["Yoichiro Hasebe"]
|
13
|
-
gem.add_development_dependency "thoughtbot-shoulda"
|
14
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
-
end
|
16
|
-
rescue LoadError
|
17
|
-
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
18
|
-
end
|
3
|
+
# frozen_string_literal: true
|
19
4
|
|
20
|
-
require
|
21
|
-
|
22
|
-
test.libs << 'lib' << 'test'
|
23
|
-
test.pattern = 'test/**/*_test.rb'
|
24
|
-
test.verbose = true
|
25
|
-
end
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
require "rake/testtask"
|
26
7
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
test.libs << 'test'
|
31
|
-
test.pattern = 'test/**/*_test.rb'
|
32
|
-
test.verbose = true
|
33
|
-
end
|
34
|
-
rescue LoadError
|
35
|
-
task :rcov do
|
36
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
37
|
-
end
|
8
|
+
Rake::TestTask.new do |t|
|
9
|
+
t.libs << "test"
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
38
11
|
end
|
39
12
|
|
40
|
-
task :
|
41
|
-
|
42
|
-
task :default => :test
|
43
|
-
|
44
|
-
require 'rake/rdoctask'
|
45
|
-
Rake::RDocTask.new do |rdoc|
|
46
|
-
if File.exist?('VERSION')
|
47
|
-
version = File.read('VERSION')
|
48
|
-
else
|
49
|
-
version = ""
|
50
|
-
end
|
51
|
-
|
52
|
-
rdoc.rdoc_dir = 'rdoc'
|
53
|
-
rdoc.title = "rubyfca #{version}"
|
54
|
-
rdoc.rdoc_files.include('README*')
|
55
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
-
end
|
13
|
+
task default: :test
|
data/bin/rubyfca
CHANGED
@@ -1,88 +1,92 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
Encoding.default_external = "UTF-8"
|
6
|
+
|
7
|
+
require_relative "../lib/rubyfca"
|
8
8
|
|
9
9
|
################ parse options ##########
|
10
10
|
|
11
|
-
opts =
|
12
|
-
version
|
13
|
-
banner
|
14
|
-
|
15
|
-
RubuFCA converts Conexp CXT data to Graphviz dot format.
|
16
|
-
|
17
|
-
Usage:
|
18
|
-
rubyfca [options] <source file> <output file>
|
19
|
-
|
20
|
-
where:
|
21
|
-
<source file>
|
22
|
-
".cxt", ".csv"
|
23
|
-
<output file>
|
24
|
-
."dot", ".png", ".jpg", or ".eps"
|
25
|
-
[options]:
|
26
|
-
EOS
|
27
|
-
|
28
|
-
opt :full, "Do not contract concept labels", :default=> false
|
29
|
-
opt :coloring, "Color concept nodes [0 = none (default), 1 = lightblue/pink, 2 = monochrome]", :default => 0
|
30
|
-
opt :straight, "Straighten edges (available when output format is either png, jpg, svg, pdf, or eps)", :default => false
|
31
|
-
opt :nodesep, "Size of separation between sister nodes (from 0.1 to 5.0)", :default => 0.4
|
32
|
-
opt :ranksep, "Size of separation between ranks (from 0.1 to 5.0)", :default => 0.2
|
33
|
-
opt :legend, "Print the legend of concept nodes (available only when using circle node shape)", :default => false
|
34
|
-
opt :circle, "Use circle shaped concept nodes", :default=> false
|
11
|
+
opts = Optimist.options do
|
12
|
+
version RubyFCA::VERSION
|
13
|
+
banner <<~USAGE
|
14
|
+
RubuFCA converts Conexp CXT data to Graphviz dot format.
|
35
15
|
|
36
|
-
|
37
|
-
|
38
|
-
Trollop::die :ranksep, "must be within 0.1 - 5.0" if (opts[:ranksep] < 0.1 || opts[:ranksep] > 5.0)
|
39
|
-
Trollop::die :nodesep, "must be within 0.1 - 5.0" if (opts[:nodesep] < 0.1 || opts[:nodesep] > 5.0)
|
40
|
-
############### main program ###############
|
16
|
+
Usage:
|
17
|
+
rubyfca [options] <source file> <output file>
|
41
18
|
|
42
|
-
|
43
|
-
|
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
|
44
34
|
end
|
45
35
|
|
46
|
-
|
47
|
-
|
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
|
48
46
|
|
49
47
|
#
|
50
48
|
# extract input and output file types
|
51
49
|
#
|
52
|
-
input_type = filename1.slice(/\.[
|
53
|
-
output_type = filename2.slice(/\.[
|
50
|
+
input_type = filename1.slice(/\.[^.]+\z/).split(//)[1..].join("")
|
51
|
+
output_type = filename2.slice(/\.[^.]+\z/).split(//)[1..].join("")
|
54
52
|
|
55
|
-
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/)
|
56
55
|
showerror("These file extensions are not (yet) supported.", 1)
|
57
56
|
end
|
58
57
|
|
59
58
|
#
|
60
|
-
# input data is kept as plain text
|
59
|
+
# input data is kept as plain text unless file type is "xlsx"
|
61
60
|
#
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
65
70
|
|
66
71
|
#
|
67
72
|
# ask for confirmation of overwriting an exisiting file
|
68
73
|
#
|
69
|
-
if
|
74
|
+
if File.exist?(filename2) && !opts[:sil]
|
70
75
|
print "#{filename2} exists and will be overwritten, OK? [y/n]"
|
71
|
-
var1 =
|
72
|
-
if /y/i !~ var1
|
73
|
-
exit;
|
74
|
-
end
|
76
|
+
var1 = $stdin.gets
|
77
|
+
exit if /y/i !~ var1
|
75
78
|
end
|
76
79
|
|
77
80
|
#
|
78
81
|
# context data is converted to a hash table
|
79
82
|
#
|
80
83
|
begin
|
81
|
-
ctxt = FormalContext.new(inputdata, input_type, !opts[:full])
|
82
|
-
ctxt.
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
ctxt = FormalContext.new(inputdata, input_type, label_contraction: !opts[:full])
|
85
|
+
ctxt.calcurate
|
86
|
+
rescue StandardError => e
|
87
|
+
pp e.message
|
88
|
+
pp e.backtrace
|
89
|
+
showerror("Source data may have problems. Process aborted.", 1)
|
86
90
|
end
|
87
91
|
|
88
92
|
#
|
@@ -90,8 +94,8 @@ end
|
|
90
94
|
#
|
91
95
|
case output_type
|
92
96
|
when "dot"
|
93
|
-
File.open(filename2, "w") do |
|
94
|
-
|
97
|
+
File.open(filename2, "w") do |file|
|
98
|
+
file.write(ctxt.generate_dot(opts))
|
95
99
|
end
|
96
100
|
when "png"
|
97
101
|
ctxt.generate_img(filename2, "png", opts)
|
@@ -1,11 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
## lib/ruby_graphviz.rb -- graphviz dot generator library
|
2
4
|
## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
|
3
|
-
## Copyright:: Copyright
|
5
|
+
## Copyright:: Copyright 2009 Yoichiro Hasebe
|
4
6
|
## License:: GNU GPL version 3
|
5
7
|
|
6
8
|
class RubyGraphviz
|
7
|
-
|
8
|
-
## Example:
|
9
|
+
## Example:
|
9
10
|
##
|
10
11
|
## g = RubyGraphviz.new("newgraph", {:rankdir => "LR", :nodesep => "0.4", :ranksep => "0.2"})
|
11
12
|
##
|
@@ -14,7 +15,7 @@ class RubyGraphviz
|
|
14
15
|
@graph_data = graph_hash
|
15
16
|
@nodes = []
|
16
17
|
@edges = []
|
17
|
-
@dot = ""
|
18
|
+
@dot = +""
|
18
19
|
create_graph
|
19
20
|
end
|
20
21
|
|
@@ -33,31 +34,31 @@ class RubyGraphviz
|
|
33
34
|
end
|
34
35
|
@dot << "]"
|
35
36
|
end
|
36
|
-
@dot << ";\n"
|
37
|
+
@dot << ";\n"
|
37
38
|
end
|
38
|
-
|
39
|
+
|
39
40
|
def finish_graph
|
40
41
|
@dot << "}\n"
|
41
42
|
end
|
42
43
|
|
43
44
|
def create_edge(edgetype, nid1, nid2, edge_hash = nil)
|
44
|
-
temp = " #{nid1
|
45
|
+
temp = " #{nid1} #{edgetype} #{nid2}"
|
45
46
|
index = 0
|
46
47
|
if edge_hash
|
47
|
-
temp << " ["
|
48
|
+
temp << " ["
|
48
49
|
edge_hash.each do |k, v|
|
49
50
|
k = k.to_s
|
50
51
|
temp << "#{k} = \"#{v}\""
|
51
52
|
index += 1
|
52
53
|
temp << ", " unless index == edge_hash.size
|
53
54
|
end
|
54
|
-
temp << "]"
|
55
|
+
temp << "]"
|
55
56
|
end
|
56
|
-
|
57
|
+
temp
|
57
58
|
end
|
58
|
-
|
59
|
+
|
59
60
|
public
|
60
|
-
|
61
|
+
|
61
62
|
## Add a subgraph to a graph (recursively)
|
62
63
|
##
|
63
64
|
## Example:
|
@@ -67,7 +68,7 @@ class RubyGraphviz
|
|
67
68
|
def subgraph(graph)
|
68
69
|
@dot << graph.to_dot.sub(/\Agraph/, "subgraph")
|
69
70
|
end
|
70
|
-
|
71
|
+
|
71
72
|
## Set default options for nodes
|
72
73
|
##
|
73
74
|
## Example:
|
@@ -105,7 +106,7 @@ class RubyGraphviz
|
|
105
106
|
@dot << "];\n"
|
106
107
|
self
|
107
108
|
end
|
108
|
-
|
109
|
+
|
109
110
|
## Create a node with its options
|
110
111
|
##
|
111
112
|
## Example:
|
@@ -113,7 +114,7 @@ class RubyGraphviz
|
|
113
114
|
## graph.node("node-01", :label => "Node 01", :fillcolor => "pink")
|
114
115
|
##
|
115
116
|
def node(node_id, node_hash = nil)
|
116
|
-
@dot << " #{node_id
|
117
|
+
@dot << " #{node_id}"
|
117
118
|
index = 0
|
118
119
|
if node_hash
|
119
120
|
@dot << " ["
|
@@ -129,22 +130,22 @@ class RubyGraphviz
|
|
129
130
|
self
|
130
131
|
end
|
131
132
|
|
132
|
-
## 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
|
133
134
|
##
|
134
135
|
## Example:
|
135
136
|
##
|
136
137
|
## graph.edge("node-01", "node-02", :label => "connecting 1 and 2", :color => "lightblue")
|
137
|
-
##
|
138
|
+
##
|
138
139
|
def edge(nid1, nid2, edge_hash = nil)
|
139
140
|
@dot << create_edge("--", nid1, nid2, edge_hash) + ";\n"
|
140
141
|
self
|
141
142
|
end
|
142
143
|
|
143
|
-
## 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
|
144
145
|
##
|
145
146
|
## Example:
|
146
147
|
## graph.arrow_edge("node-01", "node-02", :label => "from 1 to 2", :color => "lightblue")
|
147
|
-
##
|
148
|
+
##
|
148
149
|
def arrow_edge(nid1, nid2, edge_hash = nil)
|
149
150
|
@dot << create_edge("->", nid1, nid2, edge_hash) + ";\n"
|
150
151
|
self
|
@@ -156,12 +157,11 @@ class RubyGraphviz
|
|
156
157
|
@dot << "{rank=same " + create_edge("--", nid1, nid2, edge_hash) + "}\n"
|
157
158
|
self
|
158
159
|
end
|
159
|
-
|
160
|
+
|
160
161
|
## Convert graph into dot formatted data
|
161
162
|
##
|
162
163
|
def to_dot
|
163
164
|
finish_graph
|
164
|
-
@dot
|
165
|
-
return @dot
|
165
|
+
@dot.gsub(/"</m, "<").gsub(/>"/m, ">")
|
166
166
|
end
|
167
167
|
end
|