pg-verify 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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