string_pattern 2.3.0 → 2.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +116 -6
- data/lib/string/pattern/add_to_ruby.rb +12 -6
- data/lib/string/pattern/analyze.rb +5 -5
- data/lib/string/pattern/checksum.rb +106 -0
- data/lib/string/pattern/email.rb +21 -0
- data/lib/string/pattern/generate.rb +135 -147
- data/lib/string/pattern/validate.rb +102 -27
- data/lib/string_pattern/version.rb +5 -0
- data/lib/string_pattern.rb +153 -1
- metadata +9 -7
|
@@ -32,6 +32,11 @@ class StringPattern
|
|
|
32
32
|
if (not_expected_errors.include?(:min_length) or not_expected_errors.include?(:max_length)) and !not_expected_errors.include?(:length)
|
|
33
33
|
not_expected_errors.push(:length)
|
|
34
34
|
end
|
|
35
|
+
if pattern.is_a?(Regexp)
|
|
36
|
+
return false if text_to_validate.nil?
|
|
37
|
+
detected_errors = StringPattern.regexp_full_match?(pattern, text_to_validate.to_s) ? [] : [:value]
|
|
38
|
+
return StringPattern.validation_decision(detected_errors, expected_errors, not_expected_errors)
|
|
39
|
+
end
|
|
35
40
|
if pattern.kind_of?(Array) and pattern.size == 1
|
|
36
41
|
pattern = pattern[0]
|
|
37
42
|
elsif pattern.kind_of?(Array) and pattern.size > 1
|
|
@@ -54,7 +59,7 @@ class StringPattern
|
|
|
54
59
|
max_length = patt.max_length.clone
|
|
55
60
|
symbol_type = patt.symbol_type.clone
|
|
56
61
|
else
|
|
57
|
-
|
|
62
|
+
StringPattern.log_message("String pattern class not supported (#{pat.class} for #{pat})")
|
|
58
63
|
return false
|
|
59
64
|
end
|
|
60
65
|
|
|
@@ -133,7 +138,9 @@ class StringPattern
|
|
|
133
138
|
required_chars << rd if rd.size == 1
|
|
134
139
|
}
|
|
135
140
|
if (required_chars.flatten & excluded_data.flatten).size > 0
|
|
136
|
-
|
|
141
|
+
message = "pattern argument not valid on StringPattern.validate, a character cannot be required and excluded at the same time: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
|
|
142
|
+
raise StringPattern::InvalidPatternError, message if @raise_on_error
|
|
143
|
+
StringPattern.log_message(message)
|
|
137
144
|
return ""
|
|
138
145
|
end
|
|
139
146
|
end
|
|
@@ -183,38 +190,106 @@ class StringPattern
|
|
|
183
190
|
end
|
|
184
191
|
}
|
|
185
192
|
else #symbol_type=="@"
|
|
186
|
-
|
|
187
|
-
wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
|
|
188
|
-
if !(Regexp.union(*wrong) === string) #don't include any or the wrong strings
|
|
189
|
-
if string.index("@").to_i > 0 and
|
|
190
|
-
string[0..(string.index("@") - 1)].scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == string[0..(string.index("@") - 1)] and
|
|
191
|
-
string[(string.index("@") + 1)..-1].scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == string[string[(string.index("@") + 1)..-1]]
|
|
192
|
-
error_regular_expression = false
|
|
193
|
-
else
|
|
194
|
-
error_regular_expression = true
|
|
195
|
-
end
|
|
196
|
-
else
|
|
197
|
-
error_regular_expression = true
|
|
198
|
-
end
|
|
199
|
-
|
|
193
|
+
error_regular_expression = !StringPattern.valid_email?(text_to_validate)
|
|
200
194
|
if error_regular_expression
|
|
201
195
|
detected_errors.push(:value)
|
|
202
196
|
end
|
|
203
197
|
end
|
|
204
198
|
end
|
|
205
199
|
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
StringPattern.validation_decision(detected_errors, expected_errors, not_expected_errors)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.validation_decision(detected_errors, expected_errors, not_expected_errors)
|
|
204
|
+
if expected_errors.size == 0 && not_expected_errors.size == 0
|
|
205
|
+
detected_errors.uniq
|
|
206
|
+
elsif (expected_errors & detected_errors) == expected_errors && (not_expected_errors & detected_errors).empty?
|
|
207
|
+
true
|
|
208
208
|
else
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# True when +text+ matches the whole +regexp+, not a substring.
|
|
214
|
+
def self.regexp_full_match?(regexp, text)
|
|
215
|
+
return false unless text.is_a?(String)
|
|
216
|
+
anchored = Regexp.new("\\A(?:#{regexp.source})\\z", regexp.options)
|
|
217
|
+
anchored.match?(text)
|
|
218
|
+
rescue RegexpError
|
|
219
|
+
false
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Describes why +text+ does or does not match +pattern+.
|
|
223
|
+
# @return [Hash] :valid, :errors, :message, and :index (segment index for arrays)
|
|
224
|
+
def self.explain(text: nil, pattern: nil, **synonyms)
|
|
225
|
+
text = synonyms[:text_to_validate] if text.nil? && synonyms.key?(:text_to_validate)
|
|
226
|
+
text = synonyms[:validate] if text.nil? && synonyms.key?(:validate)
|
|
227
|
+
|
|
228
|
+
if pattern.is_a?(Array) && pattern.size > 1
|
|
229
|
+
return explain_array(text, pattern)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
errors = validate(text: text, pattern: pattern)
|
|
233
|
+
if errors == true || (errors.is_a?(Array) && errors.empty?)
|
|
234
|
+
{ valid: true, errors: [], message: "valid", index: nil }
|
|
235
|
+
elsif errors.is_a?(Array)
|
|
236
|
+
{ valid: false, errors: errors, message: "invalid: #{errors.join(', ')}", index: nil }
|
|
237
|
+
else
|
|
238
|
+
{ valid: false, errors: [:value], message: "invalid", index: nil }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.explain_array(text, patterns)
|
|
243
|
+
ok, index, errors = match_segments(text.to_s, patterns, 0)
|
|
244
|
+
if ok
|
|
245
|
+
{ valid: true, errors: [], message: "valid", index: nil }
|
|
246
|
+
else
|
|
247
|
+
{ valid: false, errors: errors, message: "segment #{index} does not match: #{errors.join(', ')}", index: index }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def self.match_segments(text, patterns, index)
|
|
252
|
+
if patterns.empty?
|
|
253
|
+
return [true, nil, []] if text.empty?
|
|
254
|
+
return [false, index, [:length]]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if patterns.size == 1
|
|
258
|
+
errors = validate(text: text, pattern: patterns[0])
|
|
259
|
+
return [true, nil, []] if errors == true || (errors.is_a?(Array) && errors.empty?)
|
|
260
|
+
errs = errors.is_a?(Array) ? errors : [:value]
|
|
261
|
+
return [false, index, errs]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
info = segment_bounds(patterns[0])
|
|
265
|
+
return [false, index, [:value]] if info.nil?
|
|
266
|
+
|
|
267
|
+
best = nil
|
|
268
|
+
(info[:min]..info[:max]).each do |n|
|
|
269
|
+
next if n > text.length
|
|
270
|
+
errors = validate(text: text[0, n], pattern: patterns[0])
|
|
271
|
+
next unless errors == true || (errors.is_a?(Array) && errors.empty?)
|
|
272
|
+
ok, idx, errs = match_segments(text[n..], patterns[1..], index + 1)
|
|
273
|
+
return [true, nil, []] if ok
|
|
274
|
+
best = [idx, errs] if best.nil? || idx > best[0]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
return [false, best[0], best[1]] if best
|
|
278
|
+
|
|
279
|
+
errors = validate(text: text, pattern: patterns[0])
|
|
280
|
+
errs = errors.is_a?(Array) ? errors : [:value]
|
|
281
|
+
errs = [:value] if errs.empty?
|
|
282
|
+
[false, index, errs]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def self.segment_bounds(pat)
|
|
286
|
+
pattern_like = pat.to_s.scan(/(\d+)-(\d+):(.+)/).size > 0 || pat.to_s.scan(/^!?(\d+):(.+)/).size > 0
|
|
287
|
+
if pat.is_a?(String) && (!optimistic || !pattern_like)
|
|
288
|
+
{ min: pat.length, max: pat.length }
|
|
289
|
+
elsif pat.is_a?(Symbol) || pat.is_a?(String)
|
|
290
|
+
analyzed = analyze(pat, silent: true)
|
|
291
|
+
return nil unless analyzed.is_a?(Struct)
|
|
292
|
+
{ min: analyzed.min_length, max: analyzed.max_length }
|
|
218
293
|
end
|
|
219
294
|
end
|
|
220
295
|
end
|
data/lib/string_pattern.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
SP_ADD_TO_RUBY = true if !defined?(SP_ADD_TO_RUBY)
|
|
2
|
+
require_relative "string_pattern/version"
|
|
2
3
|
require_relative "string/pattern/add_to_ruby" if SP_ADD_TO_RUBY
|
|
3
4
|
require_relative "string/pattern/analyze"
|
|
5
|
+
require_relative "string/pattern/checksum"
|
|
6
|
+
require_relative "string/pattern/email"
|
|
4
7
|
require_relative "string/pattern/generate"
|
|
5
8
|
require_relative "string/pattern/validate"
|
|
6
9
|
|
|
@@ -29,10 +32,47 @@ require_relative "string/pattern/validate"
|
|
|
29
32
|
# Array of words to be avoided from resultant strings.
|
|
30
33
|
# block_list_enabled: (TrueFalse, default: false)
|
|
31
34
|
# If true block_list will be take in consideration
|
|
35
|
+
#
|
|
36
|
+
# @example Generate a string
|
|
37
|
+
# StringPattern.generate("10:N") # => "3448910834"
|
|
38
|
+
# @example Validate and get errors
|
|
39
|
+
# StringPattern.validate(text: "ab", pattern: "6:N") # => [:min_length, :length]
|
|
32
40
|
class StringPattern
|
|
41
|
+
# Raised when an invalid pattern is used and {raise_on_error} is true.
|
|
42
|
+
InvalidPatternError = Class.new(StandardError)
|
|
43
|
+
# Raised when generation is impossible (e.g. exhausted by dont_repeat) and {raise_on_error} is true.
|
|
44
|
+
GenerationImpossibleError = Class.new(StandardError)
|
|
45
|
+
|
|
46
|
+
# Wraps Random so +rand+ matches Kernel#rand for negative limits and zero.
|
|
47
|
+
# Array#sample calls +rand+ with a positive integer.
|
|
48
|
+
class RandomSource
|
|
49
|
+
def initialize(random)
|
|
50
|
+
@random = random
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def rand(limit = nil)
|
|
54
|
+
return @random.rand if limit.nil?
|
|
55
|
+
if limit.is_a?(Integer) && limit.negative?
|
|
56
|
+
@random.rand(-limit)
|
|
57
|
+
elsif limit == 0
|
|
58
|
+
@random.rand
|
|
59
|
+
else
|
|
60
|
+
@random.rand(limit)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
33
65
|
class << self
|
|
34
|
-
|
|
66
|
+
# @return [String, nil] When set, warning messages are sent here instead of +puts+.
|
|
67
|
+
attr_accessor :logger
|
|
68
|
+
# @return [Boolean] When true, invalid patterns or impossible generation raise instead of returning "".
|
|
69
|
+
attr_accessor :raise_on_error
|
|
70
|
+
attr_accessor :national_chars, :optimistic, :dont_repeat, :cache, :cache_values, :default_infinite, :word_separator, :block_list, :block_list_enabled, :block_list_regexp
|
|
35
71
|
end
|
|
72
|
+
SCOPE_KEYS = %i[
|
|
73
|
+
dont_repeat block_list block_list_enabled block_list_regexp
|
|
74
|
+
national_chars optimistic word_separator raise_on_error logger
|
|
75
|
+
].freeze
|
|
36
76
|
@national_chars = (("a".."z").to_a + ("A".."Z").to_a).join
|
|
37
77
|
@optimistic = true
|
|
38
78
|
@cache = Hash.new()
|
|
@@ -42,6 +82,9 @@ class StringPattern
|
|
|
42
82
|
@word_separator = "_"
|
|
43
83
|
@block_list_enabled = false
|
|
44
84
|
@block_list = []
|
|
85
|
+
@logger = nil
|
|
86
|
+
@raise_on_error = false
|
|
87
|
+
@block_list_regexp = false
|
|
45
88
|
NUMBER_SET = ("0".."9").to_a
|
|
46
89
|
SPECIAL_SET = [" ", "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "{", "}", "[", "]", "'", ";", ":", "?", ">", "<", "`", "|", "/", '"']
|
|
47
90
|
ALPHA_SET_LOWER = ("a".."z").to_a
|
|
@@ -63,4 +106,113 @@ class StringPattern
|
|
|
63
106
|
@national_chars = par
|
|
64
107
|
end
|
|
65
108
|
|
|
109
|
+
def self.log_message(message)
|
|
110
|
+
if @logger
|
|
111
|
+
@logger.warn(message)
|
|
112
|
+
else
|
|
113
|
+
puts message
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns true if +text+ matches +pattern+; false otherwise. Uses validate under the hood.
|
|
118
|
+
# @param text [String] (synonyms: text_to_validate, validate)
|
|
119
|
+
# @param pattern [String, Symbol, Array]
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def self.valid?(text: nil, pattern: nil, **synonyms)
|
|
122
|
+
text = synonyms[:text_to_validate] if text.nil? && synonyms.key?(:text_to_validate)
|
|
123
|
+
text = synonyms[:validate] if text.nil? && synonyms.key?(:validate)
|
|
124
|
+
return false if text.nil? || pattern.nil?
|
|
125
|
+
result = validate(text: text, pattern: pattern, **synonyms)
|
|
126
|
+
pattern.is_a?(Array) ? result == true : result.is_a?(Array) && result.empty?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Generates up to +n+ distinct strings for +pattern+. Uses a temporary dont_repeat state.
|
|
130
|
+
# Keyword arguments, including +seed:+, are forwarded to {generate}. A seed advances one
|
|
131
|
+
# Random across the whole sample so successive values differ.
|
|
132
|
+
# @param pattern [String, Symbol, Array, Regexp]
|
|
133
|
+
# @param n [Integer]
|
|
134
|
+
# @return [Array<String>]
|
|
135
|
+
def self.sample(pattern, n, seed: nil, **options)
|
|
136
|
+
return [] if n <= 0
|
|
137
|
+
|
|
138
|
+
rng = StringPattern::RandomSource.new(seed.nil? ? Random.new : Random.new(seed))
|
|
139
|
+
old_dont = @dont_repeat
|
|
140
|
+
old_cache = @cache_values.dup
|
|
141
|
+
started = true
|
|
142
|
+
@dont_repeat = true
|
|
143
|
+
@cache_values = {}
|
|
144
|
+
results = []
|
|
145
|
+
n.times do
|
|
146
|
+
s = generate(pattern, _rng: rng, **options)
|
|
147
|
+
break if s.nil? || s.empty?
|
|
148
|
+
results << s
|
|
149
|
+
end
|
|
150
|
+
results
|
|
151
|
+
ensure
|
|
152
|
+
if started
|
|
153
|
+
@dont_repeat = old_dont
|
|
154
|
+
@cache_values = old_cache
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Runs +block+ with the given settings and restores the previous values afterwards.
|
|
159
|
+
# @return [Object] the block's result
|
|
160
|
+
def self.with(**options)
|
|
161
|
+
unknown = options.keys - SCOPE_KEYS
|
|
162
|
+
raise ArgumentError, "unknown option: #{unknown.join(', ')}" unless unknown.empty?
|
|
163
|
+
|
|
164
|
+
saved = {}
|
|
165
|
+
SCOPE_KEYS.each { |key| saved[key] = instance_variable_get("@#{key}") }
|
|
166
|
+
options.each do |key, value|
|
|
167
|
+
if key == :national_chars
|
|
168
|
+
self.national_chars = value
|
|
169
|
+
else
|
|
170
|
+
instance_variable_set("@#{key}", value)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
yield
|
|
174
|
+
ensure
|
|
175
|
+
if saved
|
|
176
|
+
saved.each do |key, value|
|
|
177
|
+
if key == :national_chars
|
|
178
|
+
self.national_chars = value unless @national_chars == value
|
|
179
|
+
else
|
|
180
|
+
instance_variable_set("@#{key}", value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Restores configuration defaults and clears pattern and generated-value caches.
|
|
187
|
+
# @return [void]
|
|
188
|
+
def self.reset!
|
|
189
|
+
self.national_chars = (("a".."z").to_a + ("A".."Z").to_a).join
|
|
190
|
+
@optimistic = true
|
|
191
|
+
@dont_repeat = false
|
|
192
|
+
@default_infinite = 10
|
|
193
|
+
@word_separator = "_"
|
|
194
|
+
@block_list_enabled = false
|
|
195
|
+
@block_list = []
|
|
196
|
+
@block_list_regexp = false
|
|
197
|
+
@logger = nil
|
|
198
|
+
@raise_on_error = false
|
|
199
|
+
@cache = {}
|
|
200
|
+
@cache_values = {}
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Generates a random UUID v4 (e.g. "550e8400-e29b-41d4-a716-446655440000").
|
|
205
|
+
# @return [String]
|
|
206
|
+
def self.uuid
|
|
207
|
+
require "securerandom"
|
|
208
|
+
SecureRandom.uuid
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Returns true if +str+ is a valid UUID v4 format (8-4-4-4-12 hex with version and variant bits).
|
|
212
|
+
# @param str [String]
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
def self.valid_uuid?(str)
|
|
215
|
+
return false unless str.is_a?(String)
|
|
216
|
+
str.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
|
|
217
|
+
end
|
|
66
218
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: string_pattern
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mario Ruiz
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: regexp_parser
|
|
@@ -39,10 +38,12 @@ email: marioruizs@gmail.com
|
|
|
39
38
|
executables: []
|
|
40
39
|
extensions: []
|
|
41
40
|
extra_rdoc_files:
|
|
41
|
+
- CHANGELOG.md
|
|
42
42
|
- LICENSE
|
|
43
43
|
- README.md
|
|
44
44
|
files:
|
|
45
45
|
- ".yardopts"
|
|
46
|
+
- CHANGELOG.md
|
|
46
47
|
- LICENSE
|
|
47
48
|
- README.md
|
|
48
49
|
- data/english/adjs.json
|
|
@@ -61,14 +62,16 @@ files:
|
|
|
61
62
|
- data/spanish/palabras9.json
|
|
62
63
|
- lib/string/pattern/add_to_ruby.rb
|
|
63
64
|
- lib/string/pattern/analyze.rb
|
|
65
|
+
- lib/string/pattern/checksum.rb
|
|
66
|
+
- lib/string/pattern/email.rb
|
|
64
67
|
- lib/string/pattern/generate.rb
|
|
65
68
|
- lib/string/pattern/validate.rb
|
|
66
69
|
- lib/string_pattern.rb
|
|
70
|
+
- lib/string_pattern/version.rb
|
|
67
71
|
homepage: https://github.com/MarioRuiz/string_pattern
|
|
68
72
|
licenses:
|
|
69
73
|
- MIT
|
|
70
74
|
metadata: {}
|
|
71
|
-
post_install_message:
|
|
72
75
|
rdoc_options: []
|
|
73
76
|
require_paths:
|
|
74
77
|
- lib
|
|
@@ -76,15 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
76
79
|
requirements:
|
|
77
80
|
- - ">="
|
|
78
81
|
- !ruby/object:Gem::Version
|
|
79
|
-
version: '0'
|
|
82
|
+
version: '3.0'
|
|
80
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
84
|
requirements:
|
|
82
85
|
- - ">="
|
|
83
86
|
- !ruby/object:Gem::Version
|
|
84
87
|
version: '0'
|
|
85
88
|
requirements: []
|
|
86
|
-
rubygems_version:
|
|
87
|
-
signing_key:
|
|
89
|
+
rubygems_version: 4.0.6
|
|
88
90
|
specification_version: 4
|
|
89
91
|
summary: 'Generate easily random strings following a simple pattern or regular expression.
|
|
90
92
|
''10-20:Xn/x/''.generate #>qBstvc6JN8ra. Also generate words in English or Spanish.
|