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/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,2 +1,13 @@
1
1
  #!/usr/bin/env rake
2
+
3
+ # frozen_string_literal: true
4
+
2
5
  require "bundler/gem_tasks"
6
+ require "rake/testtask"
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs << "test"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
12
+
13
+ task default: :test
data/bin/rubyfca CHANGED
@@ -1,86 +1,92 @@
1
1
  #!/usr/bin/env ruby
2
- # -*- coding: utf-8 -*-
3
2
 
4
- $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
5
- require 'rubyfca'
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 = Trollop::options do
11
+ opts = Optimist.options do
10
12
  version RubyFCA::VERSION
11
- banner <<-EOS
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
- end
35
- Trollop::die :coloring, "must be 0, 1, or 2" if (opts[:coloring] > 2 || opts[:coloring] < 0)
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
- if ARGV.size != 2
41
- showerror("Input and output files are not set properly", 1)
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
- filename1 = ARGV[0] #input filename
45
- filename2 = ARGV[1] #output filename
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(/\.[^\.]+\z/).split(//)[1..-1].join("")
51
- output_type = filename2.slice(/\.[^\.]+\z/).split(//)[1..-1].join("")
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/ || output_type !~ /\A(dot|png|jpg|svg|pdf|eps)\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
- f = File.open(filename1, "r")
61
- inputdata = f.read
62
- f.close
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 (File.exist?(filename2) && !opts[:sil])
74
+ if File.exist?(filename2) && !opts[:sil]
68
75
  print "#{filename2} exists and will be overwritten, OK? [y/n]"
69
- var1 = STDIN.gets;
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
- # rescue => e
82
- # puts e
83
- # showerror("Source data may have problems. Process aborted.", 1)
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 |f|
92
- f.write(ctxt.generate_dot(opts))
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
- #!/usr/bin/env ruby
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.to_s} #{edgetype} #{nid2.to_s}"
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
- return temp
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.to_s}"
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 = @dot.gsub(/\"\</m, "<").gsub(/\>\"/m, ">")
168
- return @dot
165
+ @dot.gsub(/"</m, "<").gsub(/>"/m, ">")
169
166
  end
170
167
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyFCA
2
- VERSION = "0.2.11"
4
+ VERSION = "0.3.0"
3
5
  end