strings-case 0.3.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,287 +1,389 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
3
4
  require "strscan"
4
5
 
6
+ require_relative "case/acronyms"
7
+ require_relative "case/configuration"
5
8
  require_relative "case/version"
6
9
 
7
10
  module Strings
8
- module Case
9
- DIGITS = ("0".."9").freeze
10
- UP_LETTERS = ("A".."Z").freeze
11
- 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
12
20
  DELIMITERS = [" ", "\n", "\t", "_", ".", "-", "#", "?", "!"].freeze
21
+ private_constant :DELIMITERS
22
+
23
+ # The pattern to detect word delimiters
24
+ #
25
+ # @return [Regexp]
26
+ #
27
+ # @api private
13
28
  DELIMS = Regexp.union(DELIMITERS)
14
- NONALPHANUMERIC = (32..127).map(&:chr) -
15
- (DIGITS.to_a + DOWN_LETTERS.to_a + UP_LETTERS.to_a + DELIMITERS)
16
- NONALPHAS = Regexp.union(NONALPHANUMERIC)
29
+ private_constant :DELIMS
30
+
31
+ # The pattern to detect uppercase characters
32
+ #
33
+ # @return [Regexp]
34
+ #
35
+ # @api private
17
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
18
44
  LOWERCASE = /\p{Lu}(?=\p{Ll})/.freeze
45
+ private_constant :LOWERCASE
19
46
 
20
47
  class Error < StandardError; end
21
48
 
22
- # Prevent changing case
23
- module NullCase
24
- def downcase
25
- self
26
- end
27
- alias upcase downcase
28
- alias capitalize downcase
49
+ # Global instance
50
+ #
51
+ # @return [Strings::Case]
52
+ #
53
+ # @api private
54
+ def self.__instance__
55
+ @__instance__ ||= Case.new
29
56
  end
57
+ private_class_method :__instance__
58
+
59
+ class << self
60
+ extend Forwardable
30
61
 
31
- # Convert string to camel case:
32
- # * start with a lowercase character
33
- # * every subsequent word has its first character uppercased
34
- # * 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
35
69
  #
36
70
  # @example
37
- # camelcase("foo bar baz") # => "fooBarBaz"
71
+ # strings = Strings::Case.new(acronyms: %w[HTTP XML])
38
72
  #
39
- # @param [String] string
40
- # the string to camelcase
41
- # @param [Array[String]] acronyms
73
+ # @param [Array<String>] acronyms
42
74
  # the acronyms to use to prevent modifications
43
- # @param [String] separator
44
- # the separator for linking words, by default none
45
75
  #
46
76
  # @api public
47
- def camelcase(string, acronyms: [], separator: "")
48
- res = parsecase(string, acronyms: acronyms, sep: separator, casing: :capitalize)
77
+ def initialize(acronyms: nil)
78
+ configure(acronyms: acronyms)
79
+ end
49
80
 
50
- 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
51
92
 
52
- acronyms_regex = /^(#{acronyms.join("|")})/
53
- if !acronyms.empty? && (res =~ acronyms_regex)
54
- 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
55
116
  else
56
- res[0].downcase + res[1..-1]
117
+ config.acronym(*acronyms)
57
118
  end
58
119
  end
59
- module_function :camelcase
60
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
61
142
  alias lower_camelcase camelcase
62
- module_function :lower_camelcase
63
143
 
64
- # Converts string to a constant
144
+ # Convert string to constant case
65
145
  #
66
146
  # @example
67
147
  # constantcase("foo bar baz") # => "FOO_BAR_BAZ"
68
148
  #
69
149
  # @param [String] string
70
- # the string to turn into constant
71
- # @param [Array[String]] acronyms
150
+ # the string to convert to constant case
151
+ # @param [Array<String>] acronyms
72
152
  # the acronyms to use to prevent modifications
73
153
  # @param [String] separator
74
- # the words separator, by default "_"
154
+ # the words separator, by default an underscore "_"
155
+ #
156
+ # @return [String]
75
157
  #
76
158
  # @api public
77
- def constcase(string, separator: "_")
78
- 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)
79
162
  end
80
- module_function :constcase
81
-
82
163
  alias constantcase constcase
83
- module_function :constantcase
84
164
 
85
- # Convert string to a HTTP Header
165
+ # Convert string to header case
86
166
  #
87
167
  # @example
88
168
  # headercase("foo bar baz") # = "Foo-Bar-Baz"
89
169
  #
90
170
  # @param [String] string
91
- # the string to turn into header
92
- # @param [Array[String]] acronyms
171
+ # the string to convert to header case
172
+ # @param [Array<String>] acronyms
93
173
  # the acronyms to use to prevent modifications
94
174
  # @param [String] separator
95
- # the words separator, by default "-"
175
+ # the words separator, by default a hyphen "-"
176
+ #
177
+ # @return [String]
96
178
  #
97
179
  # @api public
98
- def headercase(string, acronyms: [], separator: "-")
99
- 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
100
185
  end
101
- module_function :headercase
102
186
 
103
- # Converts string to lower case words linked by hyphenes
187
+ # Convert string to kebab case
104
188
  #
105
189
  # @example
106
190
  # kebabcase("fooBarBaz") # => "foo-bar-baz"
107
191
  #
192
+ # @example
108
193
  # kebabcase("__FOO_BAR__") # => "foo-bar"
109
194
  #
110
195
  # @param [String] string
111
- # the string to convert to dashed string
112
- # @param [Array[String]] acronyms
196
+ # the string to convert to kebab case
197
+ # @param [Array<String>] acronyms
113
198
  # the acronyms to use to prevent modifications
114
199
  # @param [String] separator
115
- # the separator for linking words, by default hyphen
200
+ # the words separator, by default a hyphen "-"
116
201
  #
117
202
  # @return [String]
118
203
  #
119
204
  # @api public
120
- def kebabcase(string, acronyms: [], separator: "-")
121
- 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)
122
208
  end
123
- module_function :kebabcase
124
-
125
209
  alias dashcase kebabcase
126
- module_function :dashcase
127
210
 
128
- # Convert string to pascal case:
129
- # * every word has its first character uppercased
130
- # * all words are compounded together
211
+ # Convert string to Pascal case
131
212
  #
132
213
  # @example
133
214
  # pascalcase("foo bar baz") # => "FooBarBaz"
134
215
  #
135
216
  # @param [String] string
136
- # the string to convert to camel case with capital letter
137
- # @param [Array[String]] acronyms
217
+ # the string to convert to Pascal case
218
+ # @param [Array<String>] acronyms
138
219
  # the acronyms to use to prevent modifications
139
220
  # @param [String] separator
140
- # the separator for linking words, by default none
221
+ # the words separator, by default an empty string
222
+ #
223
+ # @return [String]
141
224
  #
142
225
  # @api public
143
- def pascalcase(string, acronyms: [], separator: "")
144
- 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
145
231
  end
146
- module_function :pascalcase
147
-
148
232
  alias upper_camelcase pascalcase
149
- module_function :upper_camelcase
150
233
 
151
- # Convert string into a file path.
152
- #
153
- # By default uses `/` as a path separator.
234
+ # Convert string to path case
154
235
  #
155
236
  # @example
156
237
  # pathcase("foo bar baz") # => "foo/bar/baz"
157
238
  #
239
+ # @example
158
240
  # pathcase("FooBarBaz") # => "foo/bar/baz"
159
241
  #
160
242
  # @param [String] string
161
- # the string to convert to file path
162
- # @param [Array[String]] acronyms
243
+ # the string to convert to path case
244
+ # @param [Array<String>] acronyms
163
245
  # the acronyms to use to prevent modifications
164
246
  # @param [String] separator
165
- # the separator for linking words, by default `/`
247
+ # the words separator, by default a forward slash "/"
248
+ #
249
+ # @return [String]
166
250
  #
167
251
  # @api public
168
- def pathcase(string, acronyms: [], separator: "/")
169
- 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)
170
255
  end
171
- module_function :pathcase
172
256
 
173
- # Convert string int a sentence
257
+ # Convert string to sentence case
174
258
  #
175
259
  # @example
176
260
  # sentencecase("foo bar baz") # => "Foo bar baz"
177
261
  #
178
262
  # @param [String] string
179
- # the string to convert to sentence
180
- # @param [Array[String]] acronyms
263
+ # the string to convert to sentence case
264
+ # @param [Array<String>] acronyms
181
265
  # the acronyms to use to prevent modifications
182
266
  # @param [String] separator
183
- # the separator for linking words, by default a space
267
+ # the words separator, by default a space
268
+ #
269
+ # @return [String]
184
270
  #
185
271
  # @api public
186
- def sentencecase(string, acronyms: [], separator: " ")
187
- res = parsecase(string, acronyms: acronyms, sep: separator, casing: :downcase)
188
-
189
- return res if res.to_s.empty?
190
-
191
- 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
192
277
  end
193
- module_function :sentencecase
194
278
 
195
- # Convert string into a snake_case
279
+ # Convert string to snake case
196
280
  #
197
281
  # @example
198
282
  # snakecase("foo bar baz") # => "foo_bar_baz"
199
283
  #
284
+ # @example
200
285
  # snakecase("ЗдравствуйтеПривет") # => "здравствуйте_привет"
201
286
  #
287
+ # @example
202
288
  # snakecase("HTTPResponse") # => "http_response"
203
289
  #
204
290
  # @param [String] string
205
291
  # the string to convert to snake case
206
- # @param [Array[String]] acronyms
292
+ # @param [Array<String>] acronyms
207
293
  # the acronyms to use to prevent modifications
208
294
  # @param [String] separator
209
- # the separator for linking words, by default `_`
295
+ # the words separator, by default an underscore "_"
296
+ #
297
+ # @return [String]
210
298
  #
211
299
  # @api public
212
- def snakecase(string, acronyms: [], separator: "_")
213
- 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)
214
303
  end
215
- module_function :snakecase
216
-
217
304
  alias underscore snakecase
218
- module_function :underscore
219
305
 
220
- # Convert string into a title case
306
+ # Convert string to title case
221
307
  #
222
308
  # @example
223
309
  # titlecase("foo bar baz") # => "Foo Bar Baz"
224
310
  #
225
311
  # @param [String] string
226
312
  # the string to convert to title case
227
- # @param [Array[String]] acronyms
313
+ # @param [Array<String>] acronyms
228
314
  # the acronyms to use to prevent modifications
229
315
  # @param [String] separator
230
- # the separator for linking words, by default a space
316
+ # the words separator, by default a space
317
+ #
318
+ # @return [String]
231
319
  #
232
320
  # @api public
233
- def titlecase(string, acronyms: [], separator: " ")
234
- 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
235
326
  end
236
- module_function :titlecase
327
+
328
+ private
237
329
 
238
330
  # Parse string and transform to desired case
239
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
+ #
240
343
  # @api private
241
- def parsecase(string, acronyms: [], sep: "_", casing: :downcase)
344
+ def parsecase(string, acronyms: nil, sep: "_", &conversion)
242
345
  return if string.nil?
243
346
 
244
- words = split_into_words(string, sep: sep)
245
-
246
- no_case = ->(w) { acronyms.include?(w) ? w.extend(NullCase) : w }
247
-
248
- words
249
- .map(&no_case)
250
- .map(&casing)
251
- .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)
252
350
  end
253
- module_function :parsecase
254
- private_class_method :parsecase
255
351
 
256
352
  # Split string into words
257
353
  #
258
- # @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>]
259
362
  # the split words
260
363
  #
261
364
  # @api private
262
- def split_into_words(string, sep: nil)
365
+ def split_into_words(string, acronyms: nil, sep: nil)
263
366
  words = []
264
367
  word = []
265
368
  scanner = StringScanner.new(string)
266
369
 
267
370
  while !scanner.eos?
268
- if scanner.match?(UPPERCASE)
269
- char = scanner.getch
270
- if word.size <= 1 # don't allow single letter words
271
- word << char
272
- else
273
- word << char
371
+ if scanner.match?(acronyms.pattern)
372
+ unless word.empty?
274
373
  words << word.join
275
- word = []
374
+ word.clear
276
375
  end
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
277
383
  elsif scanner.match?(LOWERCASE)
278
384
  char = scanner.getch
279
- if word.size <= 1 # don't allow single letter words
280
- word << char
281
- else
282
- words << word.join
283
- word = [char]
284
- end
385
+ words << word.join unless word.empty?
386
+ word = [char]
285
387
  elsif scanner.match?(DELIMS)
286
388
  char = scanner.getch
287
389
  words << word.join unless word.empty?
@@ -290,10 +392,8 @@ module Strings
290
392
  elsif scanner.eos? && char == sep
291
393
  word = [""]
292
394
  else
293
- word = []
395
+ word.clear
294
396
  end
295
- elsif scanner.skip(NONALPHAS)
296
- # noop
297
397
  else
298
398
  word << scanner.getch
299
399
  end
@@ -303,6 +403,5 @@ module Strings
303
403
 
304
404
  words
305
405
  end
306
- module_function :split_into_words
307
406
  end # Case
308
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.3.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-12-07 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,57 +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/perf/parsecase_spec.rb
71
- - spec/spec_helper.rb
72
- - spec/unit/camelcase_spec.rb
73
- - spec/unit/constcase_spec.rb
74
- - spec/unit/extensions_spec.rb
75
- - spec/unit/headercase_spec.rb
76
- - spec/unit/kebabcase_spec.rb
77
- - spec/unit/pascalcase_spec.rb
78
- - spec/unit/pathcase_spec.rb
79
- - spec/unit/sentencecase_spec.rb
80
- - spec/unit/snakecase_spec.rb
81
- - spec/unit/titlecase_spec.rb
82
- - strings-case.gemspec
83
- - tasks/console.rake
84
- - tasks/coverage.rake
85
- - tasks/spec.rake
86
60
  homepage: https://github.com/piotrmurach/strings-case
87
61
  licenses:
88
62
  - MIT
89
63
  metadata:
90
64
  allowed_push_host: https://rubygems.org
65
+ bug_tracker_uri: https://github.com/piotrmurach/strings-case/issues
91
66
  changelog_uri: https://github.com/piotrmurach/strings-case/blob/master/CHANGELOG.md
92
67
  documentation_uri: https://www.rubydoc.info/gems/strings-case
93
68
  homepage_uri: https://github.com/piotrmurach/strings-case
69
+ rubygems_mfa_required: 'true'
94
70
  source_code_uri: https://github.com/piotrmurach/strings-case
95
- post_install_message:
71
+ post_install_message:
96
72
  rdoc_options: []
97
73
  require_paths:
98
74
  - lib
@@ -107,8 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
83
  - !ruby/object:Gem::Version
108
84
  version: '0'
109
85
  requirements: []
110
- rubygems_version: 3.0.6
111
- signing_key:
86
+ rubygems_version: 3.4.10
87
+ signing_key:
112
88
  specification_version: 4
113
89
  summary: Convert strings to different cases
114
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
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rspec-benchmark"
4
- require "active_support"
5
-
6
- RSpec.describe Strings::Case do
7
- include RSpec::Benchmark::Matchers
8
-
9
- it "changes case" do
10
- expect {
11
- Strings::Case.snakecase("fooBarBaz")
12
- }.to perform_slower_than {
13
- ActiveSupport::Inflector.underscore("fooBarBaz")
14
- }.at_most(2).times
15
- end
16
-
17
- it "allocates no more than 100 objects" do
18
- expect {
19
- Strings::Case.snakecase("fooBarBaz")
20
- }.to perform_allocation(25)
21
- end
22
- end