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.
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