knapsack_solver 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +2 -0
- data/bin/knapsack_solver +12 -0
- data/knapsack_solver.gemspec +33 -0
- data/lib/knapsack_solver.rb +2 -0
- data/lib/knapsack_solver/cli.rb +49 -0
- data/lib/knapsack_solver/cli_option_checker.rb +83 -0
- data/lib/knapsack_solver/cli_option_parser.rb +68 -0
- data/lib/knapsack_solver/dataset.rb +44 -0
- data/lib/knapsack_solver/graph_printer.rb +115 -0
- data/lib/knapsack_solver/instance.rb +48 -0
- data/lib/knapsack_solver/output_printer.rb +97 -0
- data/lib/knapsack_solver/solver.rb +118 -0
- data/lib/knapsack_solver/solving_methods/branch_and_bound.rb +111 -0
- data/lib/knapsack_solver/solving_methods/dynamic_programming.rb +116 -0
- data/lib/knapsack_solver/solving_methods/fptas.rb +47 -0
- data/lib/knapsack_solver/solving_methods/heuristic_price_weight.rb +56 -0
- data/lib/knapsack_solver/version.rb +5 -0
- data/test/knapsack_solver_matchers.rb +103 -0
- data/test/knapsack_solver_spec.rb +261 -0
- data/test/spec_helper.rb +5 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e7d04df167b3dc41b861e541d7fba44765765280
|
4
|
+
data.tar.gz: 1832b618f0828d2409583bf97f0804cd13ac5b24
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5615a26a781ae3cd094666aaf93c8d412083882a4e4c8c3eb800735d6513794e9f27ad81cda4d447dd6707eb8b18f4b73868ef92ef54792d243f804a8f936b1c
|
7
|
+
data.tar.gz: 697349d2dce6d2f8d9acdaf32e552594c30745aa787f46b7051ff2635908bafb8374bd27d2b3e00a43dbb05436c24a99514872786dc315e1e92fcf2b999d449c
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2018, Ján Sučan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/bin/knapsack_solver
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path('lib/knapsack_solver/version', __dir__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'knapsack_solver'
|
5
|
+
s.version = KnapsackSolver::VERSION
|
6
|
+
s.homepage = 'https://github.com/sucanjan/knapsack-solver'
|
7
|
+
s.license = 'MIT'
|
8
|
+
s.author = 'Jan Sucan'
|
9
|
+
s.email = 'sucanjan@fit.cvut.cz'
|
10
|
+
|
11
|
+
s.summary = '0/1 knapsack problem solver.'
|
12
|
+
s.description = <<-EOF
|
13
|
+
This gem contains command-line utility for solving 0/1 knapsack problem using
|
14
|
+
branch-and-bound method, dynamic programming, simple heuristic (weight/price)
|
15
|
+
and fully polynomial time approximation scheme.
|
16
|
+
|
17
|
+
It can measure CPU and wall-clock time spent by solving a problem, compute
|
18
|
+
relative error of the result and generate graphs from those values.
|
19
|
+
EOF
|
20
|
+
|
21
|
+
s.files = Dir['bin/*', 'lib/**/*', '*.gemspec', 'LICENSE*', 'README*', 'test/*']
|
22
|
+
s.executables = Dir['bin/*'].map { |f| File.basename(f) }
|
23
|
+
s.has_rdoc = 'yard'
|
24
|
+
|
25
|
+
s.required_ruby_version = '>= 2.2'
|
26
|
+
|
27
|
+
s.add_runtime_dependency 'gnuplot', '~> 2.6'
|
28
|
+
|
29
|
+
s.add_development_dependency 'rake', '~> 12.0'
|
30
|
+
s.add_development_dependency 'rspec', '~> 3.6'
|
31
|
+
s.add_development_dependency 'rubocop', '~> 0.50.0'
|
32
|
+
s.add_development_dependency 'yard', '~> 0.9'
|
33
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'knapsack_solver/solver'
|
2
|
+
require 'knapsack_solver/version'
|
3
|
+
require 'knapsack_solver/cli_option_parser'
|
4
|
+
require 'knapsack_solver/output_printer'
|
5
|
+
require 'knapsack_solver/graph_printer'
|
6
|
+
|
7
|
+
module KnapsackSolver
|
8
|
+
# This class implements a command-line interface for the 0/1 knapsack
|
9
|
+
# problem solver.
|
10
|
+
class CLI
|
11
|
+
# Suffix of a text file which will containg results of dataset solving
|
12
|
+
# (price, knapsack things presence, cpu time, wall clock time,
|
13
|
+
# relative_error).
|
14
|
+
RESULTS_FILNEMAE_SUFFIX = '.results'.freeze
|
15
|
+
|
16
|
+
# Suffix of a text file which will containg statistic data (average price,
|
17
|
+
# execution times, relative error)
|
18
|
+
STATS_FILNEMAE_SUFFIX = '.stats'.freeze
|
19
|
+
|
20
|
+
# Processes command-line arguments. If no option is given, converts arabic
|
21
|
+
# number to roman number and prints it to stdout.
|
22
|
+
#
|
23
|
+
# @param args [Array] the command-line arguments
|
24
|
+
def self.run(args)
|
25
|
+
options = CliOptionParser.parse(args)
|
26
|
+
return if options.nil?
|
27
|
+
datasets = args.each_with_object([]) do |file, sets|
|
28
|
+
sets << Dataset.parse(File.new(file))
|
29
|
+
end
|
30
|
+
s = Solver.new(options, datasets)
|
31
|
+
results = s.run
|
32
|
+
print_results(results, s.stats(results), options, args)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Prints output of datasets solving. Results and statistics are printed to
|
36
|
+
# stdout or to a text files. Graphs of statistic values can be created.
|
37
|
+
#
|
38
|
+
# @param results [Hash] results of dataset solvings
|
39
|
+
# @param stats [Hash] statistics from the results of dataset solvings
|
40
|
+
# @param options [Hash] Command-line line options supplied to the CLI
|
41
|
+
# @param args [Array] array of the positional command-line arguments
|
42
|
+
def self.print_results(results, stats, options, args)
|
43
|
+
OutputPrinter.new(args, RESULTS_FILNEMAE_SUFFIX, results).print(options[:output_dir])
|
44
|
+
OutputPrinter.new(args, STATS_FILNEMAE_SUFFIX, stats).print(options[:output_dir])
|
45
|
+
return unless options[:graphs_dir]
|
46
|
+
GraphPrinter.new(args, stats, options[:graphs_dir]).print
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module KnapsackSolver
|
4
|
+
# This class checks command line arguments provided to the knapsack_solver
|
5
|
+
# binary.
|
6
|
+
class CliOptionChecker
|
7
|
+
# Checks command-line options, their arguments and positional arguments
|
8
|
+
# provided to the CLI.
|
9
|
+
#
|
10
|
+
# @param opts [Hash] parsed command-line options
|
11
|
+
# @param args [Array<String>] command-line positional arguments
|
12
|
+
def self.check(opts, args)
|
13
|
+
if !opts[:branch_and_bound] && !opts[:dynamic_programming] &&
|
14
|
+
!opts[:fptas] && !opts[:heuristic]
|
15
|
+
raise StandardError, 'At least one method of solving must be requested'
|
16
|
+
end
|
17
|
+
check_fptas_options(opts)
|
18
|
+
check_directories(opts)
|
19
|
+
check_positional_arguments(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Checks command-line options and arguments used by FPTAS solving method.
|
23
|
+
#
|
24
|
+
# @param opts [Hash] parsed command-line options
|
25
|
+
def self.check_fptas_options(opts)
|
26
|
+
return if !opts[:fptas] && !opts.key?(:fptas_epsilon)
|
27
|
+
check_incomplete_fptas_options(opts)
|
28
|
+
eps = opts[:fptas_epsilon].to_f
|
29
|
+
return unless eps <= 0 || eps >= 1 || eps.to_s != opts[:fptas_epsilon]
|
30
|
+
raise StandardError,
|
31
|
+
'FPTAS epsilon must be number from range (0,1)'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Checks command-line options and arguments used by FPTAS solving
|
35
|
+
# method. Recignizes cases when mandatory FPTAS epsilon constant is
|
36
|
+
# missing or when it the constant is provided and FPTAS method is not
|
37
|
+
# requested.
|
38
|
+
#
|
39
|
+
# @param opts [Hash] parsed command-line options
|
40
|
+
def self.check_incomplete_fptas_options(opts)
|
41
|
+
raise StandardError, 'Missing FPTAS epsilon constant' if opts[:fptas] && !opts.key?(:fptas_epsilon)
|
42
|
+
return unless !opts[:fptas] && opts.key?(:fptas_epsilon)
|
43
|
+
raise StandardError,
|
44
|
+
'epsilon constant must not be provided when FPTAS is not selected'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Checks directory for result and statistic output logs, and directory for
|
48
|
+
# graph files.
|
49
|
+
#
|
50
|
+
# @param opts [Hash] parsed command-line options
|
51
|
+
def self.check_directories(opts)
|
52
|
+
check_output_directory(opts[:output_dir]) if opts[:output_dir]
|
53
|
+
check_output_directory(opts[:graphs_dir]) if opts[:graphs_dir]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Checks if at least one dataset input file was provided and if the input
|
57
|
+
# files are readable.
|
58
|
+
#
|
59
|
+
# @param args [Array<String>] positional arguments provided to the CLI
|
60
|
+
def self.check_positional_arguments(args)
|
61
|
+
raise StandardError, 'Missing datset file(s)' if args.empty?
|
62
|
+
args.each { |f| check_input_file(f) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks if an output directory exist and is writable.
|
66
|
+
#
|
67
|
+
# @param path [Path] path to output directory
|
68
|
+
def self.check_output_directory(path)
|
69
|
+
raise StandardError, "Directory '#{path}' does not exists" unless File.exist?(path)
|
70
|
+
raise StandardError, "'#{path}' is not a directory" unless File.directory?(path)
|
71
|
+
raise StandardError, "Directory '#{path}' is not writable" unless File.writable?(path)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Checks if an input file exist and is readable.
|
75
|
+
#
|
76
|
+
# @param path [Path] path to input regular file
|
77
|
+
def self.check_input_file(path)
|
78
|
+
raise StandardError, "File '#{path}' does not exists" unless File.exist?(path)
|
79
|
+
raise StandardError, "'#{path}' is not a regular file" unless File.file?(path)
|
80
|
+
raise StandardError, "File '#{path}' is not readable" unless File.readable?(path)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'knapsack_solver/cli_option_checker'
|
3
|
+
|
4
|
+
module KnapsackSolver
|
5
|
+
# This class parses command line arguments provided to the knapsack_solver
|
6
|
+
# binary.
|
7
|
+
class CliOptionParser
|
8
|
+
# Message that describes how to use this CLI utility.
|
9
|
+
USAGE_MESSAGE = 'Usage: knapsack_solver OPTIONS DATASET_FILE...'.freeze
|
10
|
+
|
11
|
+
# Parses command-line arguments and removes them from the array of
|
12
|
+
# arguments.
|
13
|
+
#
|
14
|
+
# @param [Array] arguments the command-line arguments.
|
15
|
+
# @return [Hash] hash of recognized options.
|
16
|
+
#
|
17
|
+
# rubocop:disable Metrics/AbcSize, Metric/MethodLength, Metric/BlockLength
|
18
|
+
def self.parse(arguments)
|
19
|
+
options = {}
|
20
|
+
parser = OptionParser.new do |opts|
|
21
|
+
opts.banner = USAGE_MESSAGE
|
22
|
+
opts.on('-b', '--branch-and-bound', 'Use branch and boung method of solving') do
|
23
|
+
options[:branch_and_bound] = true
|
24
|
+
end
|
25
|
+
opts.on('-d', '--dynamic-programming', 'Use dynamic programming for solving') do
|
26
|
+
options[:dynamic_programming] = true
|
27
|
+
end
|
28
|
+
opts.on('-f', '--fptas', 'Use FPTAS for solving') do
|
29
|
+
options[:fptas] = true
|
30
|
+
end
|
31
|
+
opts.on('-r', '--heuristic', 'Use brute force method of solving') do
|
32
|
+
options[:heuristic] = true
|
33
|
+
end
|
34
|
+
opts.on('-e', '--fptas-epsilon EPS', 'Relative error for FPTAS from range (0,1)') do |eps|
|
35
|
+
options[:fptas_epsilon] = eps
|
36
|
+
end
|
37
|
+
opts.on('-o', '--output DIR', 'Directory for output log files') do |dir|
|
38
|
+
options[:output_dir] = dir
|
39
|
+
end
|
40
|
+
opts.on('-g', '--graphs DIR', 'Directory for graphs') do |dir|
|
41
|
+
options[:graphs_dir] = dir
|
42
|
+
end
|
43
|
+
opts.on('-v', '--version', 'Show program version') do
|
44
|
+
options[:version] = true
|
45
|
+
end
|
46
|
+
opts.on_tail('-h', '--help', 'Show this help message') do
|
47
|
+
options[:help] = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
parser.parse!(arguments)
|
51
|
+
process_help_and_version_opts(options, arguments, parser.to_s)
|
52
|
+
end
|
53
|
+
# rubocop:enable Metrics/AbcSize, Metric/MethodLength, Metric/BlockLength
|
54
|
+
|
55
|
+
def self.process_help_and_version_opts(options, arguments, usage_msg)
|
56
|
+
if !options[:help] && !options[:version]
|
57
|
+
CliOptionChecker.check(options, arguments)
|
58
|
+
return options
|
59
|
+
end
|
60
|
+
if options[:help]
|
61
|
+
puts usage_msg
|
62
|
+
elsif options[:version]
|
63
|
+
puts "knapsack_solver #{KnapsackSolver::VERSION}"
|
64
|
+
end
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'knapsack_solver/instance'
|
2
|
+
|
3
|
+
module KnapsackSolver
|
4
|
+
# This class represents a set of 0/1 knapsack problem instances.
|
5
|
+
class Dataset
|
6
|
+
# Initializes set of 0/1 knapsack problem instances.
|
7
|
+
#
|
8
|
+
# @param id [Integer] Dataset ID number.
|
9
|
+
# @param instances [Array<Instance>] set of the 0/1 knapsack problem instances.
|
10
|
+
def initialize(id, instances)
|
11
|
+
@id = id
|
12
|
+
@instances = instances
|
13
|
+
end
|
14
|
+
|
15
|
+
# Parses set of a 0/1 knapsack problem instances from a character stream.
|
16
|
+
#
|
17
|
+
# @param stream [#eof?,#readline,#each_line] character stream holding the dataset.
|
18
|
+
# @return [Dataset] dataset instance parsed from the stream.
|
19
|
+
def self.parse(stream)
|
20
|
+
id = parse_id(stream)
|
21
|
+
instances = stream.each_line.with_object([]) { |l, o| o << Instance.parse(l) }
|
22
|
+
raise StandardError, 'dataset: missing instances' if instances.empty?
|
23
|
+
Dataset.new(id, instances)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Parses ID of a 0/1 knapsack problem dataset from a character stream.
|
27
|
+
#
|
28
|
+
# @param stream [#eof?,#readline,#each_line] character stream holding the dataset.
|
29
|
+
# @return [Integer] dataset ID number.
|
30
|
+
def self.parse_id(stream)
|
31
|
+
raise StandardError, 'dataset: missing ID' if stream.eof?
|
32
|
+
s = stream.readline.split
|
33
|
+
raise StandardError, 'dataset: first line does not contain ID' if s.size != 1
|
34
|
+
begin
|
35
|
+
raise StandardError, 'dataset: ID is negative' if Integer(s.first) < 0
|
36
|
+
rescue ArgumentError
|
37
|
+
raise StandardError, 'dataset: ID is not an integer'
|
38
|
+
end
|
39
|
+
Integer(s.first)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :id, :instances
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'gnuplot'
|
2
|
+
|
3
|
+
module KnapsackSolver
|
4
|
+
# This class provides support for making graphs from statistics of datasets
|
5
|
+
# solving results. It uses Gnuplot and also generates a Gnuplot config file
|
6
|
+
# for each generated graph.
|
7
|
+
class GraphPrinter
|
8
|
+
# Initializes printer for graph data (graphs, Gnuplot config files).
|
9
|
+
#
|
10
|
+
# @param dataset_filenames [Array<String>] dataset filenames
|
11
|
+
# @param stats [Hash] statistics of results
|
12
|
+
# @param out_dir [String] statistics of results to print
|
13
|
+
def initialize(dataset_filenames, stats, out_dir)
|
14
|
+
@dataset_basenames = file_basenames(dataset_filenames)
|
15
|
+
@stats = stats
|
16
|
+
@out_dir = out_dir
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create graphs from statistics and Gnuplot configuration files.
|
20
|
+
def print
|
21
|
+
stats_to_datasets.each do |title, ds|
|
22
|
+
ofn = File.join(@out_dir, title + '.png')
|
23
|
+
plot(title, ds, ofn)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
# Create graph.
|
30
|
+
#
|
31
|
+
# @param title [String] title of the graph
|
32
|
+
# @param data [Array<Gnuplot::DataSet>] Gnuplot datasets to plot
|
33
|
+
# @param filename [String] path to the output image file
|
34
|
+
def plot(title, data, filename)
|
35
|
+
Gnuplot.open do |gp|
|
36
|
+
Gnuplot::Plot.new(gp, &plot_config(title, 'dataset', 'y', data, filename))
|
37
|
+
end
|
38
|
+
File.open(File.join(File.dirname(filename), File.basename(filename, '.png') + '.gnuplot'), 'w') do |gp|
|
39
|
+
Gnuplot::Plot.new(gp, &plot_config(title, 'dataset', 'y', data, filename))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates Gnuplot datasets from statistics.
|
44
|
+
#
|
45
|
+
# @return [Array<Gnuplot::DataSet>] Gnuplot datasets created from the statistics.
|
46
|
+
def stats_to_datasets
|
47
|
+
graphs = @stats.values.first.values.first.first.keys
|
48
|
+
x_data = @stats.keys
|
49
|
+
datasets(graphs, x_data)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Creates Gnuplot datasets from statistics.
|
53
|
+
#
|
54
|
+
# @param graphs [Array] array of graph titles
|
55
|
+
# @param x_data [Array] array of X axis values
|
56
|
+
def datasets(graphs, x_data)
|
57
|
+
graphs.each_with_object({}) do |g, gnuplot_datasets|
|
58
|
+
@stats.each_value do |s|
|
59
|
+
gnuplot_datasets[g.to_s] = s.each_key.with_object([]) do |method, o|
|
60
|
+
o << plot_dataset(method.to_s, x_data, @stats.map { |_, v| v[method].first[g] })
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create dataset from provided title, X axis data and Y axis data.
|
67
|
+
#
|
68
|
+
# @param title [String] Gnuplot dataset title
|
69
|
+
# @param x_data [Array] Array of X values
|
70
|
+
# @param y_data [Array] Array of Y values
|
71
|
+
# @return [Gnuplot::DataSet] Gnuplot dataset.
|
72
|
+
def plot_dataset(title, x_data, y_data)
|
73
|
+
Gnuplot::DataSet.new([x_data, y_data]) { |ds| ds.title = escape_gnuplot_special_chars(title) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates Gnuplot plot configuration (configuration text lines).
|
77
|
+
#
|
78
|
+
# @param title [String] graph title
|
79
|
+
# @param xlabel [String] label of X axis
|
80
|
+
# @param ylabel [String] label of Y axis
|
81
|
+
# @param plot_datasets [Array<Gnuplot::DataSet>] Gnuplot datasets for plotting
|
82
|
+
# @param out_file [String] output file
|
83
|
+
# @return [lambda] Lambda for setting plot configuration.
|
84
|
+
def plot_config(title, xlabel, ylabel, plot_datasets, out_file)
|
85
|
+
lambda do |plot|
|
86
|
+
plot.term('png')
|
87
|
+
plot.output(out_file)
|
88
|
+
plot.title("'#{escape_gnuplot_special_chars(title)}'")
|
89
|
+
plot.ylabel("'#{escape_gnuplot_special_chars(ylabel)}'")
|
90
|
+
plot.xlabel("'#{escape_gnuplot_special_chars(xlabel)}'")
|
91
|
+
plot.key('outside')
|
92
|
+
plot.data = plot_datasets
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Escapes Gnuplot special characters.
|
97
|
+
#
|
98
|
+
# @param str [String] a string
|
99
|
+
# @return [Strnig] the string with Gnuplot special chars escaped
|
100
|
+
def escape_gnuplot_special_chars(str)
|
101
|
+
# underscore means subscript in Gnuplot
|
102
|
+
str.gsub('_', '\_')
|
103
|
+
end
|
104
|
+
|
105
|
+
# Gets basenames of supplied file paths.
|
106
|
+
#
|
107
|
+
# @param paths [Array<String>] path to files
|
108
|
+
# @return [Array<String>] basenames of the paths
|
109
|
+
def file_basenames(paths)
|
110
|
+
paths.each_with_object([]) do |path, basenames|
|
111
|
+
basenames << File.basename(path, File.extname(path))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|