rubyfca 0.2.10 → 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,56 +1,13 @@
1
- require 'rubygems'
2
- require 'rake'
1
+ #!/usr/bin/env rake
3
2
 
4
- begin
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 'rake/testtask'
21
- Rake::TestTask.new(:test) do |test|
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
- begin
28
- require 'rcov/rcovtask'
29
- Rcov::RcovTask.new do |test|
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 :test => :check_dependencies
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
- $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
5
- require 'trollop'
6
- require 'rubyfca'
7
- require 'ruby_graphviz'
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 = Trollop::options do
12
- version = File.read(File.dirname(__FILE__) + "/../VERSION")
13
- banner <<-EOS
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
- end
37
- Trollop::die :coloring, "must be 0, 1, or 2" if (opts[:coloring] > 2 || opts[:coloring] < 0)
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
- if ARGV.size != 2
43
- 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
44
34
  end
45
35
 
46
- filename1 = ARGV[0] #input filename
47
- 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
48
46
 
49
47
  #
50
48
  # extract input and output file types
51
49
  #
52
- input_type = filename1.slice(/\.[^\.]+\z/).split(//)[1..-1].join("")
53
- 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("")
54
52
 
55
- 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/)
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
- f = File.open(filename1, "r")
63
- inputdata = f.read
64
- 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
65
70
 
66
71
  #
67
72
  # ask for confirmation of overwriting an exisiting file
68
73
  #
69
- if (File.exist?(filename2) && !opts[:sil])
74
+ if File.exist?(filename2) && !opts[:sil]
70
75
  print "#{filename2} exists and will be overwritten, OK? [y/n]"
71
- var1 = STDIN.gets;
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.calculate
83
- # rescue => e
84
- # puts e
85
- # showerror("Source data may have problems. Process aborted.", 1)
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 |f|
94
- f.write(ctxt.generate_dot(opts))
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 2010 Yoichiro Hasebe
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.to_s} #{edgetype} #{nid2.to_s}"
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
- return temp
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.to_s}"
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 = @dot.gsub(/\"\</m, "<").gsub(/\>\"/m, ">")
165
- return @dot
165
+ @dot.gsub(/"</m, "<").gsub(/>"/m, ">")
166
166
  end
167
167
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyFCA
4
+ VERSION = "0.3.0"
5
+ end