pg-verify 0.1.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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +98 -0
  5. data/README.md +29 -0
  6. data/Rakefile +62 -0
  7. data/bin/console +15 -0
  8. data/bin/pg-verify.rb +18 -0
  9. data/bin/setup +8 -0
  10. data/calc.ebnf +21 -0
  11. data/data/config/pg-verify.yml +66 -0
  12. data/data/nusmv.sample.smv +179 -0
  13. data/data/project-template/.gitignore.resource +4 -0
  14. data/data/project-template/.pg-verify.yml +0 -0
  15. data/data/project-template/README.md +18 -0
  16. data/data/project-template/addon/.keep +0 -0
  17. data/data/project-template/program-graph.rb.resource +103 -0
  18. data/devpg +5 -0
  19. data/doc/examples/railroad_crossing.rb +61 -0
  20. data/doc/examples/train-tree.rb +43 -0
  21. data/doc/examples/weidezaun.rb +99 -0
  22. data/doc/examples/weidezaun.txt +29 -0
  23. data/doc/expose/definition.png +0 -0
  24. data/doc/expose/diagram.png +0 -0
  25. data/doc/expose/expose.md +359 -0
  26. data/doc/expose/validity.png +0 -0
  27. data/exe/pg-verify +4 -0
  28. data/integration_tests/ruby_dsl/001_states.rb +10 -0
  29. data/integration_tests/ruby_dsl/002_transitions.rb +10 -0
  30. data/integration_tests/ruby_dsl/003_actions.rb +14 -0
  31. data/integration_tests/ruby_dsl/004_guards.rb +18 -0
  32. data/integration_tests/ruby_dsl/005_variables.rb +16 -0
  33. data/integration_tests/ruby_dsl/006_state_variables.rb +26 -0
  34. data/integration_tests/ruby_dsl/007_variable_initialization.rb +28 -0
  35. data/integration_tests/ruby_dsl/008_state_initialization.rb +19 -0
  36. data/integration_tests/ruby_dsl/009_shared_variables.rb +26 -0
  37. data/integration_tests/ruby_dsl/010_complex_guards.rb +18 -0
  38. data/integration_tests/ruby_dsl/011_complex_actions.rb +16 -0
  39. data/integration_tests/ruby_dsl/012_error_components.rb +9 -0
  40. data/integration_tests/ruby_dsl/013_hazards.rb +25 -0
  41. data/integration_tests/ruby_dsl/014_tau_transitions.rb +26 -0
  42. data/integration_tests/ruby_dsl/015_basic_dcca.rb +19 -0
  43. data/integration_tests/ruby_dsl/016_pressure_tank.rb +146 -0
  44. data/lib/pg-verify/cli/cli.rb +235 -0
  45. data/lib/pg-verify/core/cmd_runner.rb +151 -0
  46. data/lib/pg-verify/core/core.rb +38 -0
  47. data/lib/pg-verify/core/extensions/array_extensions.rb +11 -0
  48. data/lib/pg-verify/core/extensions/enumerable_extensions.rb +19 -0
  49. data/lib/pg-verify/core/extensions/nil_extensions.rb +7 -0
  50. data/lib/pg-verify/core/extensions/string_extensions.rb +84 -0
  51. data/lib/pg-verify/core/shell/colorizer.rb +136 -0
  52. data/lib/pg-verify/core/shell/shell.rb +0 -0
  53. data/lib/pg-verify/core/util.rb +146 -0
  54. data/lib/pg-verify/doctor/doctor.rb +180 -0
  55. data/lib/pg-verify/ebnf_parser/ast.rb +31 -0
  56. data/lib/pg-verify/ebnf_parser/ebnf_parser.rb +26 -0
  57. data/lib/pg-verify/ebnf_parser/expression_parser.rb +177 -0
  58. data/lib/pg-verify/ebnf_parser/expression_parser2.rb +422 -0
  59. data/lib/pg-verify/ebnf_parser/expressions.ebnf +33 -0
  60. data/lib/pg-verify/ebnf_parser/expressions.peg +52 -0
  61. data/lib/pg-verify/ebnf_parser/parser_result.rb +26 -0
  62. data/lib/pg-verify/interpret/component_context.rb +125 -0
  63. data/lib/pg-verify/interpret/graph_context.rb +85 -0
  64. data/lib/pg-verify/interpret/interpret.rb +142 -0
  65. data/lib/pg-verify/interpret/pg_script.rb +72 -0
  66. data/lib/pg-verify/interpret/spec/ltl_builder.rb +90 -0
  67. data/lib/pg-verify/interpret/spec/spec_context.rb +32 -0
  68. data/lib/pg-verify/interpret/spec/spec_set_context.rb +67 -0
  69. data/lib/pg-verify/interpret/transition_context.rb +55 -0
  70. data/lib/pg-verify/model/allocation_set.rb +28 -0
  71. data/lib/pg-verify/model/assignment.rb +34 -0
  72. data/lib/pg-verify/model/component.rb +40 -0
  73. data/lib/pg-verify/model/dcca/hazard.rb +16 -0
  74. data/lib/pg-verify/model/dcca.rb +67 -0
  75. data/lib/pg-verify/model/expression.rb +106 -0
  76. data/lib/pg-verify/model/graph.rb +58 -0
  77. data/lib/pg-verify/model/model.rb +10 -0
  78. data/lib/pg-verify/model/parsed_expression.rb +77 -0
  79. data/lib/pg-verify/model/simulation/trace.rb +43 -0
  80. data/lib/pg-verify/model/simulation/variable_state.rb +23 -0
  81. data/lib/pg-verify/model/source_location.rb +45 -0
  82. data/lib/pg-verify/model/specs/spec.rb +44 -0
  83. data/lib/pg-verify/model/specs/spec_result.rb +25 -0
  84. data/lib/pg-verify/model/specs/spec_set.rb +43 -0
  85. data/lib/pg-verify/model/specs/specification.rb +50 -0
  86. data/lib/pg-verify/model/transition.rb +41 -0
  87. data/lib/pg-verify/model/validation/assignment_to_state_variable_validation.rb +26 -0
  88. data/lib/pg-verify/model/validation/empty_state_set_validation.rb +18 -0
  89. data/lib/pg-verify/model/validation/errors.rb +119 -0
  90. data/lib/pg-verify/model/validation/foreign_assignment_validation.rb +30 -0
  91. data/lib/pg-verify/model/validation/unknown_token_validation.rb +35 -0
  92. data/lib/pg-verify/model/validation/validation.rb +23 -0
  93. data/lib/pg-verify/model/variable.rb +47 -0
  94. data/lib/pg-verify/model/variable_set.rb +84 -0
  95. data/lib/pg-verify/nusmv/nusmv.rb +23 -0
  96. data/lib/pg-verify/nusmv/runner.rb +124 -0
  97. data/lib/pg-verify/puml/puml.rb +23 -0
  98. data/lib/pg-verify/shell/loading/line_animation.rb +36 -0
  99. data/lib/pg-verify/shell/loading/loading_animation.rb +80 -0
  100. data/lib/pg-verify/shell/loading/loading_prompt.rb +43 -0
  101. data/lib/pg-verify/shell/loading/no_animation.rb +20 -0
  102. data/lib/pg-verify/shell/shell.rb +30 -0
  103. data/lib/pg-verify/simulation/simulation.rb +7 -0
  104. data/lib/pg-verify/simulation/simulator.rb +90 -0
  105. data/lib/pg-verify/simulation/state.rb +53 -0
  106. data/lib/pg-verify/transform/hash_transformation.rb +104 -0
  107. data/lib/pg-verify/transform/nusmv_transformation.rb +261 -0
  108. data/lib/pg-verify/transform/puml_transformation.rb +89 -0
  109. data/lib/pg-verify/transform/transform.rb +8 -0
  110. data/lib/pg-verify/version.rb +5 -0
  111. data/lib/pg-verify.rb +47 -0
  112. data/pg-verify.gemspec +38 -0
  113. data/sig/pg-verify.rbs +4 -0
  114. metadata +226 -0
@@ -0,0 +1,151 @@
1
+ module PgVerify
2
+
3
+ module Core
4
+
5
+ module CMDRunner
6
+
7
+ # Runs the command and raises on error.
8
+ def self.run_cmd(cmd, env_variables = {}, include_stderr: false)
9
+ start_time = Time.new
10
+ output, err, status = Open3.capture3(env_variables, cmd)
11
+ delta_seconds = (Time.new - start_time).to_i
12
+ raise CMDRunnerError.new(cmd, output, err, delta_seconds) unless status.success?
13
+ return output + err if include_stderr
14
+ return output
15
+ end
16
+
17
+ # Runs the command and returns stdout on success. Returns the specified value otherwise.
18
+ def self.run_or_return(cmd, fail_output = nil)
19
+ output, err, status = Open3.capture3(cmd)
20
+ return status.success? ? output : fail_output
21
+ end
22
+
23
+ # Runs the command and returns stdout, stderr and the status.
24
+ # Status will be true only if the command succeeded.
25
+ def self.run_for_result(cmd)
26
+ output, err, status = run_for_result_with_plain_status(cmd)
27
+ return output, err, status.success?
28
+ end
29
+
30
+ def self.run_for_result_with_plain_status(cmd)
31
+ output, err, status = Open3.capture3(cmd)
32
+ return output, err, status
33
+ end
34
+
35
+ def self.run_for_exit_code(cmd)
36
+ output, err, status = Open3.capture3(cmd)
37
+ return status.exitstatus
38
+ end
39
+
40
+ def self.drop_into_shell()
41
+ shell = run_cmd("echo ${SHELL}")
42
+ system("#{shell}")
43
+ end
44
+
45
+ def self.run_in_screen(cmd, session_name)
46
+ run_cmd("screen -S #{session_name} -dm bash -c '#{cmd}'")
47
+ end
48
+
49
+ def self.run_with_timeout(cmd)
50
+ require 'timeout'
51
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thread|
52
+ Timeout.timeout(2) do
53
+ wait_thread.join
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.with_drop_on_fail(shell, intent: nil, &blk)
59
+ begin
60
+ blk.call()
61
+ rescue CMDRunnerError => e
62
+ shell.info("Output:\n#{e.output}")
63
+ shell.error("The action that was attempted did fail after #{e.delta_seconds} seconds!")
64
+ shell.info("The exact command was: #{e.command.c_command}")
65
+ shell.info("Intent was: #{intent}") unless intent.nil?
66
+ shell.info("You can try to resolve this problem manually.")
67
+ raise e unless shell.ask_confirm(nil, question: "Do you want to open a shell and try our luck?")
68
+ drop_into_shell()
69
+ shell.info("Welcome back :D")
70
+
71
+ op1 = "Run the command again to see if it works."
72
+ op2 = "Just continue and pretend the command did succeed.\n(This will return '' to the caller)"
73
+ op3 = "Assume the command failed."
74
+ sel = shell.select([op1, op2, op3], prompt: "How shall we continue?").first
75
+ case sel
76
+ when op1
77
+ return with_drop_on_fail(shell, blk, intent: intent,)
78
+ when op2
79
+ return ""
80
+ when op3
81
+ raise e
82
+ end
83
+ end
84
+ end
85
+
86
+ # Runs the specified command and waits for it to complete. Calls the specified
87
+ # block for each line the process writes to stdout and err.
88
+ # Will return two things:
89
+ # 1. The complete output as a string
90
+ # 2. The result as a bool, where true means success.
91
+ # If raise_on_fail is set to true it will raise directly on fail and not return the results.
92
+ # If gulp_interrupt is set to true will also gulp interrupts.
93
+ def self.run_and_follow(cmd, raise_on_fail: true, gulp_interrupt: false, timeout: nil, &blk)
94
+ start_time = Time.new
95
+ output = ""
96
+ success = nil
97
+
98
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thread|
99
+ begin
100
+ # Listen to stdout & stderr in seperate threads.
101
+ [stdout, stderr].each do |stream|
102
+ Thread.new do
103
+ begin
104
+ until (line = stream.gets).nil? do
105
+ blk.call(line) unless blk.nil?
106
+ output += line
107
+ end
108
+ rescue IOError => e
109
+ # Expected when the stream is closed
110
+ end
111
+ end
112
+ end
113
+ unless timeout.nil?
114
+ require 'timeout'
115
+ Timeout.timeout(timeout) { wait_thread.join }
116
+ else
117
+ wait_thread.join
118
+ end
119
+ delta_seconds = Time.new - start_time
120
+ success = wait_thread.value.success?
121
+ raise CMDRunnerError.new(cmd, output, "", delta_seconds) if raise_on_fail && !success
122
+ rescue SignalException => e
123
+ raise e unless gulp_interrupt
124
+ success = false
125
+ end
126
+ return output, success
127
+ end
128
+ end
129
+
130
+ def self.command_exists?(command)
131
+ !run_or_return("command -v '#{command}'", nil).nil?
132
+ end
133
+
134
+ class CMDRunnerError < PgVerify::Core::Error
135
+ def initialize(cmd, output, err, delta_seconds)
136
+ @cmd, @output, @err, @delta_seconds = cmd, output, err, delta_seconds
137
+ end
138
+
139
+ def formatted()
140
+ title = "Running '#{@cmd}' failed after #{@delta_seconds} seconds:"
141
+
142
+ body = "$ #{@cmd.c_string} \n\n #{@err}"
143
+ return title, body
144
+ end
145
+
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,38 @@
1
+ module PgVerify
2
+
3
+ module Core
4
+
5
+ class Error < StandardError
6
+
7
+ def formatted()
8
+ raise "Not implemented in subclass #{self.class}"
9
+ end
10
+
11
+ def to_formatted()
12
+ title, body, hint = self.formatted()
13
+
14
+ message = []
15
+
16
+ error_label = " ✖ ERROR ".bg_error
17
+ message << "#{error_label} #{title.c_error}" unless title.nil?
18
+
19
+ indent = " ".bg_error + " "
20
+ message << "#{indent}\n#{body.indented(str: indent)}\n#{indent}" unless body.nil?
21
+
22
+ indent = " ".bg_warn + " "
23
+ message << backtrace.map.map {|l| "#{indent}#{l.c_warn}"}.join("\n") if Settings.full_stack_trace
24
+
25
+ indent = " ".bg_sidenote + " "
26
+ message << hint.split("\n").map {|l| "#{indent}#{l.c_sidenote}"}.join("\n") unless hint.nil?
27
+
28
+ return message.join("\n")
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ # Require all module files
38
+ Dir[File.join(__dir__, "**", '*.rb')].sort.each { |file| require file }
@@ -0,0 +1,11 @@
1
+ class Array
2
+
3
+ def gsub(elem, substitute)
4
+ return self.map { |orig| orig == elem ? substitute : orig }
5
+ end
6
+
7
+ def blank?()
8
+ return empty?()
9
+ end
10
+
11
+ end
@@ -0,0 +1,19 @@
1
+ module Enumerable
2
+
3
+ def powerset
4
+ array = self.to_a()
5
+ ret = Enumerator.new {|ps|
6
+ array.size.times {|n|
7
+ array.combination(n).each(&ps.method(:yield))
8
+ }
9
+ }
10
+ return ret.to_a + [array]
11
+ end
12
+
13
+ def subset?(other)
14
+ other = other.to_a()
15
+ array = self.to_a()
16
+ return other.all? { |item| array.include?(item) }
17
+ end
18
+
19
+ end
@@ -0,0 +1,7 @@
1
+ class NilClass
2
+
3
+ def blank?()
4
+ true
5
+ end
6
+
7
+ end
@@ -0,0 +1,84 @@
1
+ class String
2
+
3
+ def integer?
4
+ self.to_i.to_s == self
5
+ end
6
+
7
+ def snake_case
8
+ self.strip()
9
+ .gsub(/ +/, "_")
10
+ .gsub(/::/, '/')
11
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
12
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
13
+ .tr("-", "_")
14
+ .downcase()
15
+ end
16
+
17
+ def camel_case
18
+ return self if self !~ /_/ && self =~ /[A-Z]+.*/
19
+ split('_').map{|e| e.capitalize}.join
20
+ end
21
+
22
+ def grep(str, e)
23
+ self.split("\n").select { |l| l.include?(str) }
24
+ end
25
+
26
+ def display_length()
27
+ str = PgVerify::Colorizer.uncolorize(self)
28
+ str.length() + ( str.count("\t") * 4 )
29
+ end
30
+
31
+ def line_combine(other, separator: " ")
32
+ PgVerify::StringUtil.line_combine(self, other, separator: separator)
33
+ end
34
+
35
+ def indented(num: 1, str: " " * 4)
36
+ PgVerify::StringUtil.indented(self, num_indents: num, indent_string: str)
37
+ end
38
+
39
+ def remove_before(substring)
40
+ split = self.split(substring)
41
+ return "" if split.length == 1
42
+ return split[1, split.length].join(substring)
43
+ end
44
+
45
+ def blank?
46
+ self.empty?
47
+ end
48
+
49
+ def limit_lines(num_lines, separator: "...")
50
+ return "" if num_lines == 0
51
+ split = self.split("\n")
52
+ return self unless split.length > num_lines
53
+ first_part = split[0, num_lines / 2]
54
+ second_part = split[split.length - (num_lines - first_part.length), split.length]
55
+ return first_part.join("\n") + "\n#{separator}\n" + second_part.join("\n")
56
+ end
57
+
58
+ def shorten(length)
59
+ return self if self.length <= length
60
+ return self[0, [length - 3, 1].max] + "..."
61
+ end
62
+
63
+ def labelize(bg: :darkgreen, fg: :white)
64
+ "◖".send(:"c_#{bg}") + " #{self} ".send(:"bg_#{bg}").send(:"c_#{fg}").c_bold + "◗".send(:"c_#{bg}")
65
+ end
66
+
67
+ def file?()
68
+ File.file?(self)
69
+ end
70
+
71
+ def directory?()
72
+ File.directory?(self)
73
+ end
74
+
75
+ # Support this method for Ruby <= 2.3
76
+ unless self.method_defined?(:delete_prefix)
77
+ def delete_prefix(prefix)
78
+ self.respond_to?(:delete_prefix)
79
+ return unless self.start_with?(prefix)
80
+ return self[prefix.length, self.length - 1]
81
+ end
82
+ end
83
+
84
+ end
@@ -0,0 +1,136 @@
1
+ require "rainbow"
2
+
3
+ module PgVerify
4
+
5
+ # A Module for easy coloration of strings with theme support.
6
+ # After #attach is called strings can be colored using one of any c_ or bg_ methods
7
+ # e.g: "Hello".c_cyan, "Hello".bg_blue
8
+ # Calls can also by chained:
9
+ # e.g: "Hello".c_red.bg_blue.c_bold
10
+ #
11
+ # A theme is a hash of keys to values. Values can be:
12
+ # 1. Hex values. e.g '#00AABB'
13
+ # 2. Color names that are defined elswhere. e.g: 'red'
14
+ # 3. Arrays of values. e.g [ 'white', '_#000000', 'bold' ]
15
+ # If a value starts with an underscore (e.g _red) the color will
16
+ # be used as the background color.
17
+ # Strings can then be colored using the c_<key> method.
18
+ # e.g: mycolor => [ "#FFFFFF", "_black" ]
19
+ # mystyle => [ "mycolor", "bold" ]
20
+ # "Hello".c_mystyle <- Colored white on black in bold
21
+ module Colorizer
22
+
23
+ def self.send_call(rainbow, color_expr)
24
+ prefix = color_expr.start_with?("_") ? "bg" : "c"
25
+ color_expr = color_expr.sub("_", "")
26
+
27
+ # If the color expression is a HEX (e.g #FF3400) ..
28
+ if color_expr.start_with?("#")
29
+ # Send the call directly to Rainbow
30
+ return rainbow.send(prefix == "bg" ? :background : :color, color_expr)
31
+ else
32
+ # Otherwise forward the call
33
+ return rainbow.send("#{prefix}_#{color_expr}".to_sym)
34
+ end
35
+ end
36
+
37
+ def self.define_methods(hash)
38
+ Rainbow::X11ColorNames::NAMES.each do |color_name, _|
39
+ define_method "c_#{color_name}".to_sym do
40
+ Rainbow(self).color(color_name.to_sym)
41
+ end
42
+ define_method "bg_#{color_name}".to_sym do
43
+ Rainbow(self).send(:background, color_name)
44
+ end
45
+ end
46
+
47
+ hash.each do |key, color_names|
48
+ array = color_names.is_a?(Array) ? color_names : [ color_names ]
49
+ define_method "c_#{key}".to_sym do
50
+ rainbow = Rainbow(self)
51
+ array.each { |color_name|
52
+ rainbow = Colorizer.send_call(rainbow, color_name)
53
+ }
54
+ rainbow
55
+ end
56
+ define_method "bg_#{key}".to_sym do
57
+ rainbow = Rainbow(self)
58
+ array.each { |color_name|
59
+ rainbow = Colorizer.send_call(rainbow, "_#{color_name}")
60
+ }
61
+ rainbow
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.uncolorize(string)
67
+ string.gsub(/\e\[([;\d]+)?m/, '')
68
+ end
69
+
70
+ def self.color?(string)
71
+ !!/\e\[([;\d]+)?m/.match(string)
72
+ end
73
+
74
+ def color(color)
75
+ method = "c_#{color}".to_sym
76
+ return self unless self.respond_to?(method)
77
+ self.send(method)
78
+ end
79
+
80
+ def c_bold
81
+ Rainbow(self).bold
82
+ end
83
+
84
+ def c_italic
85
+ Rainbow(self).italic
86
+ end
87
+
88
+ def c_underline
89
+ Rainbow(self).underline
90
+ end
91
+
92
+ def c_none
93
+ self
94
+ end
95
+
96
+ def color_bg(color)
97
+ Rainbow(self).background(color)
98
+ end
99
+
100
+ def color_regex(hash)
101
+ hash.each do |key, val|
102
+ self.gsub!(Regexp.new(key)) { |match|
103
+ "#{match.color(val)}"
104
+ }
105
+ end
106
+ self
107
+ end
108
+
109
+ def color_unique()
110
+ r, g, b = Colorizer.unique_color(self)
111
+ return Rainbow(self).color(r, g, b)
112
+ end
113
+
114
+ def bg_color_unique()
115
+ r, g, b = Colorizer.unique_color(self)
116
+ return Rainbow(self).background(r, g, b)
117
+ end
118
+
119
+ # def self.unique_color(string)
120
+ # r = Random.new(Integer("0x#{Digest::SHA256.hexdigest(string)}"))
121
+ # number = r.rand(0..360)
122
+ # r, g, b = VsuColorUtil.hsl_to_rgb(number, 60, 50)
123
+ # return r, g, b
124
+ # end
125
+
126
+ def self.attach(theme, use_colors: true)
127
+ define_methods(theme)
128
+ use_colors = Settings.use_colors
129
+ use_colors &&= Settings.use_colors_in_pipe if !$stdout.isatty
130
+ Rainbow.enabled = use_colors
131
+ String.class_eval { include PgVerify::Colorizer }
132
+ end
133
+
134
+ end
135
+
136
+ end
File without changes
@@ -0,0 +1,146 @@
1
+ module PgVerify
2
+
3
+
4
+ module TimeUtil
5
+
6
+ SECONDS_IN_SECOND = 1
7
+ SECONDS_IN_MINUTE = SECONDS_IN_SECOND * 60
8
+ SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60
9
+ SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
10
+ SECONDS_IN_WEEK = SECONDS_IN_DAY * 7
11
+ SECONDS_IN_MONTH = SECONDS_IN_WEEK * 4
12
+ SECONDS_IN_YEAR = SECONDS_IN_MONTH * 12
13
+
14
+ def self.duration_string(seconds, short: false)
15
+ i_seconds = seconds.to_i
16
+ return "~#{'%.2f' % (seconds*1000)}#{short ? "ms" : " milliseconds"}" if i_seconds == 0
17
+ return duration_to_h(i_seconds, short: short)
18
+ end
19
+
20
+ def self.duration_to_h(seconds, short: false)
21
+ seconds = seconds.to_i
22
+ {
23
+ SECONDS_IN_YEAR => ( short ? "y" : " year" ),
24
+ SECONDS_IN_MONTH => ( short ? "M" : " month" ),
25
+ SECONDS_IN_WEEK => ( short ? "w" : " week" ),
26
+ SECONDS_IN_DAY => ( short ? "d" : " day" ),
27
+ SECONDS_IN_HOUR => ( short ? "h" : " hour" ),
28
+ SECONDS_IN_MINUTE => ( short ? "m" : " minute" ),
29
+ SECONDS_IN_SECOND => ( short ? "s" : " second" )
30
+ }.each do |unit_seconds, unit_string|
31
+ units = seconds / unit_seconds
32
+ unit_string = unit_string + "s" if !short && units != 1
33
+ return "#{units}#{unit_string} #{duration_to_h(seconds % unit_seconds, short: short)}".strip unless units == 0
34
+ end
35
+ nil
36
+ end
37
+
38
+ def self.ago_h(start_time, short: false)
39
+ duration_to_h((Time.now - start_time).to_i, short: short) + " ago"
40
+ end
41
+
42
+ def self.timestamp(time = Time.new)
43
+ return time.utc.to_i
44
+ end
45
+
46
+ def self.from_timestamp(ts)
47
+ time = Time.at(ts.to_i).utc
48
+ return time
49
+ end
50
+
51
+ end
52
+
53
+ class StringUtil
54
+
55
+ def self.make_unique(string, strings, &blk)
56
+ base_string = string
57
+ index = 0
58
+ while strings.include?(string)
59
+ index += 1
60
+ string = blk.call(base_string, index)
61
+ end
62
+ string
63
+ end
64
+
65
+ def self.limit_width(string, width)
66
+ return string if string.nil? || string.length <= width
67
+ return string.chars.each_slice(width).map(&:join).join("\n")
68
+ end
69
+
70
+ def self.auto_complete(string, options)
71
+ perfect_match = options.select { |o| o == string }.uniq
72
+ return perfect_match unless perfect_match.empty?
73
+ options.select { |o| o.start_with?(string) }.uniq
74
+ end
75
+
76
+ def self.levenshtein_suggest(string, options, suggestions: 5)
77
+ options.map { |o| [o, levenshtein_distance(string, o)] }
78
+ .sort_by{ |a| a[1] }
79
+ .vsu_limit(suggestions)
80
+ .map { |a| a[0] }
81
+ end
82
+
83
+ def self.line_combine(string1, string2, separator: " ")
84
+ return string2 if string1.empty?
85
+ lines1, lines2 = string1.split("\n"), string2.split("\n")
86
+ both = [lines1, lines2]
87
+ height = both.map(&:length).max
88
+ l_width = lines1.map(&:display_length).max
89
+
90
+ # Fill up empty lines to match height
91
+ both.each { |lines| loop { break if lines.length >= height; lines << "" } }
92
+
93
+ # Fill up left lines to align right side
94
+ lines1 = lines1.map { |l| l + " " * (l_width - l.display_length) }
95
+
96
+ # Combine left and right.
97
+ string = (0...height).map { |index|
98
+ lines1[index] + separator + lines2[index]
99
+ }.join("\n")
100
+
101
+ return string
102
+ end
103
+
104
+ def self.shorten_unique(strings)
105
+ # TODO: Implement
106
+ return strings.each_with_index.map { |s, i| [s, i.to_s] }.to_h
107
+
108
+ # chars = ".- _".chars
109
+ # regex = /#{chars.map { |c| "\\#{c}" }.join("|")}/
110
+ # map = {}
111
+ # strings.each do |str|
112
+ # index = 0
113
+ # loop do
114
+ # split = str.gsub(regex, " ").split
115
+ # short = split.map { |word| word[0, index] }.join("")
116
+ # puts short
117
+ # sleep(1)
118
+ # next if map.values.include?(short)
119
+ # map[str] = short
120
+ # break
121
+ # end
122
+
123
+ # end
124
+
125
+ # map
126
+ end
127
+
128
+ def self.indented(string, num_indents: 1, indent_string: "\t")
129
+ string.split("\n").map { |l| "#{indent_string * num_indents}#{l}" }.join("\n")
130
+ end
131
+
132
+ def self.levenshtein_distance(a, b)
133
+ a, b = a.downcase, b.downcase
134
+ costs = Array(0..b.length) # i == 0
135
+ (1..a.length).each do |i|
136
+ costs[0], nw = i, i - 1 # j == 0; nw is lev(i-1, j)
137
+ (1..b.length).each do |j|
138
+ costs[j], nw = [costs[j] + 1, costs[j-1] + 1, a[i-1] == b[j-1] ? nw : nw + 1].min, costs[j]
139
+ end
140
+ end
141
+ costs[b.length]
142
+ end
143
+
144
+ end
145
+
146
+ end