strings-case 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/strings/case.rb CHANGED
@@ -1,304 +1,407 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+ require "strscan"
5
+
6
+ require_relative "case/acronyms"
7
+ require_relative "case/configuration"
3
8
  require_relative "case/version"
4
9
 
5
10
  module Strings
6
- module Case
7
- DIGITS = ("0".."9").freeze
8
- UP_LETTERS = ("A".."Z").freeze
9
- DOWN_LETTERS = ("a".."z").freeze
11
+ # Responsible for converting strings to common cases
12
+ #
13
+ # @api public
14
+ class Case
15
+ # The word delimiters
16
+ #
17
+ # @return [Array<String>]
18
+ #
19
+ # @api private
10
20
  DELIMITERS = [" ", "\n", "\t", "_", ".", "-", "#", "?", "!"].freeze
11
- NONALPHANUMERIC = (32..127).map(&:chr) -
12
- (DIGITS.to_a + DOWN_LETTERS.to_a + UP_LETTERS.to_a + DELIMITERS)
13
- UPCASE = /(?<!\p{Lu})\p{Lu}$/.freeze
21
+ private_constant :DELIMITERS
22
+
23
+ # The pattern to detect word delimiters
24
+ #
25
+ # @return [Regexp]
26
+ #
27
+ # @api private
28
+ DELIMS = Regexp.union(DELIMITERS)
29
+ private_constant :DELIMS
30
+
31
+ # The pattern to detect uppercase characters
32
+ #
33
+ # @return [Regexp]
34
+ #
35
+ # @api private
36
+ UPPERCASE = /^(\p{Ll}|\p{Digit})\p{Lu}/.freeze
37
+ private_constant :UPPERCASE
38
+
39
+ # The pattern to detect lowercase characters
40
+ #
41
+ # @return [Regexp]
42
+ #
43
+ # @api private
14
44
  LOWERCASE = /\p{Lu}(?=\p{Ll})/.freeze
45
+ private_constant :LOWERCASE
15
46
 
16
47
  class Error < StandardError; end
17
48
 
18
- # Prevent changing case
19
- module NullCase
20
- def downcase
21
- self
22
- end
23
- alias upcase downcase
24
- alias capitalize downcase
49
+ # Global instance
50
+ #
51
+ # @return [Strings::Case]
52
+ #
53
+ # @api private
54
+ def self.__instance__
55
+ @__instance__ ||= Case.new
25
56
  end
57
+ private_class_method :__instance__
58
+
59
+ class << self
60
+ extend Forwardable
26
61
 
27
- # Convert string to camel case:
28
- # * start with a lowercase character
29
- # * every subsequent word has its first character uppercased
30
- # * all words are compounded together
62
+ delegate %i[camelcase constcase constantcase dashcase
63
+ headercase kebabcase lower_camelcase
64
+ pascalcase pathcase sentencecase snakecase
65
+ titlecase underscore upper_camelcase] => :__instance__
66
+ end
67
+
68
+ # Create a Strings::Case instance
31
69
  #
32
70
  # @example
33
- # camelcase("foo bar baz") # => "fooBarBaz"
71
+ # strings = Strings::Case.new(acronyms: %w[HTTP XML])
34
72
  #
35
- # @param [String] string
36
- # the string to camelcase
37
- # @param [Array[String]] acronyms
73
+ # @param [Array<String>] acronyms
38
74
  # the acronyms to use to prevent modifications
39
- # @param [String] separator
40
- # the separator for linking words, by default none
41
75
  #
42
76
  # @api public
43
- def camelcase(string, acronyms: [], separator: "")
44
- res = parsecase(string, acronyms: acronyms, sep: separator, casing: :capitalize)
77
+ def initialize(acronyms: nil)
78
+ configure(acronyms: acronyms)
79
+ end
45
80
 
46
- return res if res.to_s.empty?
81
+ # Access configuration
82
+ #
83
+ # @example
84
+ # strings.config
85
+ #
86
+ # @return [Strings::Case::Configuration]
87
+ #
88
+ # @api public
89
+ def config
90
+ @config ||= Configuration.new
91
+ end
47
92
 
48
- acronyms_regex = /^(#{acronyms.join("|")})/
49
- if !acronyms.empty? && (res =~ acronyms_regex)
50
- res
93
+ # Configure acronyms
94
+ #
95
+ # @example
96
+ # strings = Strings::Case.new
97
+ # strings.configure do |config|
98
+ # config.acronym "HTTP"
99
+ # end
100
+ #
101
+ # @example
102
+ # strings = Strings::Case.new
103
+ # strings.configure(acronyms: %w[HTTP XML])
104
+ #
105
+ # @param [Array<String>] acronyms
106
+ # the acronyms to use to prevent modifications
107
+ #
108
+ # @yield [Strings::Case::Configuration]
109
+ #
110
+ # @return [void]
111
+ #
112
+ # @api public
113
+ def configure(acronyms: nil)
114
+ if block_given?
115
+ yield config
51
116
  else
52
- res[0].downcase + res[1..-1]
117
+ config.acronym(*acronyms)
53
118
  end
54
119
  end
55
- module_function :camelcase
56
120
 
121
+ # Convert string to camel case
122
+ #
123
+ # @example
124
+ # camelcase("foo bar baz") # => "fooBarBaz"
125
+ #
126
+ # @param [String] string
127
+ # the string to convert to camel case
128
+ # @param [Array<String>] acronyms
129
+ # the acronyms to use to prevent modifications
130
+ # @param [String] separator
131
+ # the words separator, by default an empty string
132
+ #
133
+ # @return [String]
134
+ #
135
+ # @api public
136
+ def camelcase(string, acronyms: config.acronyms, separator: "")
137
+ acronyms = Acronyms.from(acronyms)
138
+ parsecase(string, acronyms: acronyms, sep: separator) do |word, i|
139
+ acronyms.fetch(word) || (i.zero? ? word.downcase : word.capitalize)
140
+ end
141
+ end
57
142
  alias lower_camelcase camelcase
58
- module_function :lower_camelcase
59
143
 
60
- # Converts string to a constant
144
+ # Convert string to constant case
61
145
  #
62
146
  # @example
63
147
  # constantcase("foo bar baz") # => "FOO_BAR_BAZ"
64
148
  #
65
149
  # @param [String] string
66
- # the string to turn into constant
67
- # @param [Array[String]] acronyms
150
+ # the string to convert to constant case
151
+ # @param [Array<String>] acronyms
68
152
  # the acronyms to use to prevent modifications
69
153
  # @param [String] separator
70
- # the words separator, by default "_"
154
+ # the words separator, by default an underscore "_"
155
+ #
156
+ # @return [String]
71
157
  #
72
158
  # @api public
73
- def constcase(string, separator: "_")
74
- parsecase(string, sep: separator, casing: :upcase)
159
+ def constcase(string, acronyms: config.acronyms, separator: "_")
160
+ acronyms = Acronyms.from(acronyms)
161
+ parsecase(string, acronyms: acronyms, sep: separator, &:upcase)
75
162
  end
76
- module_function :constcase
77
-
78
163
  alias constantcase constcase
79
- module_function :constantcase
80
164
 
81
- # Convert string to a HTTP Header
165
+ # Convert string to header case
82
166
  #
83
167
  # @example
84
168
  # headercase("foo bar baz") # = "Foo-Bar-Baz"
85
169
  #
86
170
  # @param [String] string
87
- # the string to turn into header
88
- # @param [Array[String]] acronyms
171
+ # the string to convert to header case
172
+ # @param [Array<String>] acronyms
89
173
  # the acronyms to use to prevent modifications
90
174
  # @param [String] separator
91
- # the words separator, by default "-"
175
+ # the words separator, by default a hyphen "-"
176
+ #
177
+ # @return [String]
92
178
  #
93
179
  # @api public
94
- def headercase(string, acronyms: [], separator: "-")
95
- parsecase(string, acronyms: acronyms, sep: separator, casing: :capitalize)
180
+ def headercase(string, acronyms: config.acronyms, separator: "-")
181
+ acronyms = Acronyms.from(acronyms)
182
+ parsecase(string, acronyms: acronyms, sep: separator) do |word|
183
+ (acronym = acronyms.fetch(word)) ? acronym : word.capitalize
184
+ end
96
185
  end
97
- module_function :headercase
98
186
 
99
- # Converts string to lower case words linked by hyphenes
187
+ # Convert string to kebab case
100
188
  #
101
189
  # @example
102
190
  # kebabcase("fooBarBaz") # => "foo-bar-baz"
103
191
  #
192
+ # @example
104
193
  # kebabcase("__FOO_BAR__") # => "foo-bar"
105
194
  #
106
195
  # @param [String] string
107
- # the string to convert to dashed string
108
- # @param [Array[String]] acronyms
196
+ # the string to convert to kebab case
197
+ # @param [Array<String>] acronyms
109
198
  # the acronyms to use to prevent modifications
110
199
  # @param [String] separator
111
- # the separator for linking words, by default hyphen
200
+ # the words separator, by default a hyphen "-"
112
201
  #
113
202
  # @return [String]
114
203
  #
115
204
  # @api public
116
- def kebabcase(string, acronyms: [], separator: "-")
117
- parsecase(string, acronyms: acronyms, sep: separator)
205
+ def kebabcase(string, acronyms: config.acronyms, separator: "-")
206
+ acronyms = Acronyms.from(acronyms)
207
+ parsecase(string, acronyms: acronyms, sep: separator, &:downcase)
118
208
  end
119
- module_function :kebabcase
120
-
121
209
  alias dashcase kebabcase
122
- module_function :dashcase
123
210
 
124
- # Convert string to pascal case:
125
- # * every word has its first character uppercased
126
- # * all words are compounded together
211
+ # Convert string to Pascal case
127
212
  #
128
213
  # @example
129
214
  # pascalcase("foo bar baz") # => "FooBarBaz"
130
215
  #
131
216
  # @param [String] string
132
- # the string to convert to camel case with capital letter
133
- # @param [Array[String]] acronyms
217
+ # the string to convert to Pascal case
218
+ # @param [Array<String>] acronyms
134
219
  # the acronyms to use to prevent modifications
135
220
  # @param [String] separator
136
- # the separator for linking words, by default none
221
+ # the words separator, by default an empty string
222
+ #
223
+ # @return [String]
137
224
  #
138
225
  # @api public
139
- def pascalcase(string, acronyms: [], separator: "")
140
- parsecase(string, acronyms: acronyms, sep: separator, casing: :capitalize)
226
+ def pascalcase(string, acronyms: config.acronyms, separator: "")
227
+ acronyms = Acronyms.from(acronyms)
228
+ parsecase(string, acronyms: acronyms, sep: separator) do |word|
229
+ acronyms.fetch(word) || word.capitalize
230
+ end
141
231
  end
142
- module_function :pascalcase
143
-
144
232
  alias upper_camelcase pascalcase
145
- module_function :upper_camelcase
146
233
 
147
- # Convert string into a file path.
148
- #
149
- # By default uses `/` as a path separator.
234
+ # Convert string to path case
150
235
  #
151
236
  # @example
152
237
  # pathcase("foo bar baz") # => "foo/bar/baz"
153
238
  #
239
+ # @example
154
240
  # pathcase("FooBarBaz") # => "foo/bar/baz"
155
241
  #
156
242
  # @param [String] string
157
- # the string to convert to file path
158
- # @param [Array[String]] acronyms
243
+ # the string to convert to path case
244
+ # @param [Array<String>] acronyms
159
245
  # the acronyms to use to prevent modifications
160
246
  # @param [String] separator
161
- # the separator for linking words, by default `/`
247
+ # the words separator, by default a forward slash "/"
248
+ #
249
+ # @return [String]
162
250
  #
163
251
  # @api public
164
- def pathcase(string, acronyms: [], separator: "/")
165
- parsecase(string, acronyms: acronyms, sep: separator)
252
+ def pathcase(string, acronyms: config.acronyms, separator: "/")
253
+ acronyms = Acronyms.from(acronyms)
254
+ parsecase(string, acronyms: acronyms, sep: separator, &:downcase)
166
255
  end
167
- module_function :pathcase
168
256
 
169
- # Convert string int a sentence
257
+ # Convert string to sentence case
170
258
  #
171
259
  # @example
172
260
  # sentencecase("foo bar baz") # => "Foo bar baz"
173
261
  #
174
262
  # @param [String] string
175
- # the string to convert to sentence
176
- # @param [Array[String]] acronyms
263
+ # the string to convert to sentence case
264
+ # @param [Array<String>] acronyms
177
265
  # the acronyms to use to prevent modifications
178
266
  # @param [String] separator
179
- # the separator for linking words, by default a space
267
+ # the words separator, by default a space
268
+ #
269
+ # @return [String]
180
270
  #
181
271
  # @api public
182
- def sentencecase(string, acronyms: [], separator: " ")
183
- res = parsecase(string, acronyms: acronyms, sep: separator, casing: :downcase)
184
-
185
- return res if res.to_s.empty?
186
-
187
- res[0].upcase + res[1..-1]
272
+ def sentencecase(string, acronyms: config.acronyms, separator: " ")
273
+ acronyms = Acronyms.from(acronyms)
274
+ parsecase(string, acronyms: acronyms, sep: separator) do |word, i|
275
+ acronyms.fetch(word) || (i.zero? ? word.capitalize : word.downcase)
276
+ end
188
277
  end
189
- module_function :sentencecase
190
278
 
191
- # Convert string into a snake_case
279
+ # Convert string to snake case
192
280
  #
193
281
  # @example
194
282
  # snakecase("foo bar baz") # => "foo_bar_baz"
195
283
  #
284
+ # @example
196
285
  # snakecase("ЗдравствуйтеПривет") # => "здравствуйте_привет"
197
286
  #
287
+ # @example
198
288
  # snakecase("HTTPResponse") # => "http_response"
199
289
  #
200
290
  # @param [String] string
201
291
  # the string to convert to snake case
202
- # @param [Array[String]] acronyms
292
+ # @param [Array<String>] acronyms
203
293
  # the acronyms to use to prevent modifications
204
294
  # @param [String] separator
205
- # the separator for linking words, by default `_`
295
+ # the words separator, by default an underscore "_"
296
+ #
297
+ # @return [String]
206
298
  #
207
299
  # @api public
208
- def snakecase(string, acronyms: [], separator: "_")
209
- parsecase(string, acronyms: acronyms, sep: separator)
300
+ def snakecase(string, acronyms: config.acronyms, separator: "_")
301
+ acronyms = Acronyms.from(acronyms)
302
+ parsecase(string, acronyms: acronyms, sep: separator, &:downcase)
210
303
  end
211
- module_function :snakecase
212
-
213
304
  alias underscore snakecase
214
- module_function :underscore
215
305
 
216
- # Convert string into a title case
306
+ # Convert string to title case
217
307
  #
218
308
  # @example
219
309
  # titlecase("foo bar baz") # => "Foo Bar Baz"
220
310
  #
221
311
  # @param [String] string
222
312
  # the string to convert to title case
223
- # @param [Array[String]] acronyms
313
+ # @param [Array<String>] acronyms
224
314
  # the acronyms to use to prevent modifications
225
315
  # @param [String] separator
226
- # the separator for linking words, by default a space
316
+ # the words separator, by default a space
317
+ #
318
+ # @return [String]
227
319
  #
228
320
  # @api public
229
- def titlecase(string, acronyms: [], separator: " ")
230
- parsecase(string, acronyms: acronyms, sep: separator, casing: :capitalize)
321
+ def titlecase(string, acronyms: config.acronyms, separator: " ")
322
+ acronyms = Acronyms.from(acronyms)
323
+ parsecase(string, acronyms: acronyms, sep: separator) do |word|
324
+ acronyms.fetch(word) || word.capitalize
325
+ end
231
326
  end
232
- module_function :titlecase
327
+
328
+ private
233
329
 
234
330
  # Parse string and transform to desired case
235
331
  #
332
+ # @param [String] string
333
+ # the string to convert to a given case
334
+ # @param [Strings::Case::Acronyms] acronyms
335
+ # the acronyms to use to parse words
336
+ # @param [String] sep
337
+ # the words separator, by default an underscore "_"
338
+ #
339
+ # @yield [word, index]
340
+ #
341
+ # @return [String]
342
+ #
236
343
  # @api private
237
- def parsecase(string, acronyms: [], sep: "_", casing: :downcase)
344
+ def parsecase(string, acronyms: nil, sep: "_", &conversion)
238
345
  return if string.nil?
239
346
 
240
- words = split_into_words(string, sep: sep)
241
-
242
- no_case = ->(w) { acronyms.include?(w) ? w.extend(NullCase) : w }
243
-
244
- words
245
- .map(&no_case)
246
- .map(&casing)
247
- .join(sep)
347
+ none_or_index = conversion.arity <= 1 ? :map : :with_index
348
+ split_into_words(string, acronyms: acronyms, sep: sep)
349
+ .map.send(none_or_index, &conversion).join(sep)
248
350
  end
249
- module_function :parsecase
250
- private_class_method :parsecase
251
351
 
252
352
  # Split string into words
253
353
  #
254
- # @return [Array[String]]
354
+ # @param [String] string
355
+ # the string to split into words
356
+ # @param [Strings::Case::Acronyms] acronyms
357
+ # the acronyms to use to split words
358
+ # @param [String] sep
359
+ # the separator to use to split words
360
+ #
361
+ # @return [Array<String>]
255
362
  # the split words
256
363
  #
257
364
  # @api private
258
- def split_into_words(string, sep: nil)
365
+ def split_into_words(string, acronyms: nil, sep: nil)
259
366
  words = []
260
367
  word = []
261
- last = string.length - 1
262
-
263
- string.each_char.with_index do |char, i|
264
- combine = word[-1].to_s + char
368
+ scanner = StringScanner.new(string)
265
369
 
266
- if combine =~ UPCASE
267
- if word.size <= 1 # don't allow single letter words
268
- word << char
269
- else
370
+ while !scanner.eos?
371
+ if scanner.match?(acronyms.pattern)
372
+ unless word.empty?
270
373
  words << word.join
271
- word = [char]
374
+ word.clear
272
375
  end
273
- elsif combine =~ LOWERCASE
274
- letter = word.pop
275
- if word.size <= 1 # don't allow single letter words
276
- word << letter << char
277
- else
278
- words << word.join
279
- word = [letter, char]
280
- end
281
- elsif DELIMITERS.include?(char)
376
+ scanner.scan(acronyms.pattern)
377
+ words << scanner.matched
378
+ elsif scanner.match?(UPPERCASE)
379
+ char = scanner.getch
380
+ word << char
381
+ words << word.join
382
+ word.clear
383
+ elsif scanner.match?(LOWERCASE)
384
+ char = scanner.getch
282
385
  words << word.join unless word.empty?
283
- if i.zero? && char == sep
386
+ word = [char]
387
+ elsif scanner.match?(DELIMS)
388
+ char = scanner.getch
389
+ words << word.join unless word.empty?
390
+ if scanner.pos == 1 && char == sep
284
391
  words << ""
392
+ elsif scanner.eos? && char == sep
393
+ word = [""]
285
394
  else
286
- word = []
395
+ word.clear
287
396
  end
288
- elsif NONALPHANUMERIC.include?(char)
289
- # noop
290
397
  else
291
- word << char
292
- end
293
-
294
- if last == i
295
- word = [""] if char == sep
296
- words << word.join unless word.empty?
398
+ word << scanner.getch
297
399
  end
298
400
  end
299
401
 
402
+ words << word.join unless word.empty?
403
+
300
404
  words
301
405
  end
302
- module_function :split_into_words
303
406
  end # Case
304
407
  end # Strings
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strings-case
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-23 00:00:00.000000000 Z
11
+ date: 2023-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '1.5'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '1.5'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rake
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -42,56 +28,47 @@ dependencies:
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
- - - "~>"
31
+ - - ">="
46
32
  - !ruby/object:Gem::Version
47
33
  version: '3.0'
48
34
  type: :development
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
- - - "~>"
38
+ - - ">="
53
39
  - !ruby/object:Gem::Version
54
40
  version: '3.0'
55
41
  description: Convert strings to different cases
56
42
  email:
57
- - me@piotrmurach.com
43
+ - piotr@piotrmurach.com
58
44
  executables: []
59
45
  extensions: []
60
- extra_rdoc_files: []
46
+ extra_rdoc_files:
47
+ - README.md
48
+ - CHANGELOG.md
49
+ - LICENSE.txt
61
50
  files:
62
51
  - CHANGELOG.md
63
52
  - LICENSE.txt
64
53
  - README.md
65
- - Rakefile
66
54
  - lib/strings-case.rb
67
55
  - lib/strings/case.rb
56
+ - lib/strings/case/acronyms.rb
57
+ - lib/strings/case/configuration.rb
68
58
  - lib/strings/case/extensions.rb
69
59
  - lib/strings/case/version.rb
70
- - spec/spec_helper.rb
71
- - spec/unit/camelcase_spec.rb
72
- - spec/unit/constcase_spec.rb
73
- - spec/unit/extensions_spec.rb
74
- - spec/unit/headercase_spec.rb
75
- - spec/unit/kebabcase_spec.rb
76
- - spec/unit/pascalcase_spec.rb
77
- - spec/unit/pathcase_spec.rb
78
- - spec/unit/sentencecase_spec.rb
79
- - spec/unit/snakecase_spec.rb
80
- - spec/unit/titlecase_spec.rb
81
- - strings-case.gemspec
82
- - tasks/console.rake
83
- - tasks/coverage.rake
84
- - tasks/spec.rake
85
60
  homepage: https://github.com/piotrmurach/strings-case
86
61
  licenses:
87
62
  - MIT
88
63
  metadata:
89
64
  allowed_push_host: https://rubygems.org
65
+ bug_tracker_uri: https://github.com/piotrmurach/strings-case/issues
90
66
  changelog_uri: https://github.com/piotrmurach/strings-case/blob/master/CHANGELOG.md
91
67
  documentation_uri: https://www.rubydoc.info/gems/strings-case
92
68
  homepage_uri: https://github.com/piotrmurach/strings-case
69
+ rubygems_mfa_required: 'true'
93
70
  source_code_uri: https://github.com/piotrmurach/strings-case
94
- post_install_message:
71
+ post_install_message:
95
72
  rdoc_options: []
96
73
  require_paths:
97
74
  - lib
@@ -106,8 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
83
  - !ruby/object:Gem::Version
107
84
  version: '0'
108
85
  requirements: []
109
- rubygems_version: 3.0.6
110
- signing_key:
86
+ rubygems_version: 3.4.10
87
+ signing_key:
111
88
  specification_version: 4
112
89
  summary: Convert strings to different cases
113
90
  test_files: []
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- require "bundler/gem_tasks"
2
-
3
- FileList["tasks/**/*.rake"].each(&method(:import))
4
-
5
- desc "Run all specs"
6
- task ci: %w[ spec ]
7
-
8
- task default: :spec