strings-case 0.2.0 → 0.4.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.
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