tins 1.32.0 → 1.44.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.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/.contexts/code_comment.rb +23 -0
  3. data/.contexts/full.rb +31 -0
  4. data/.contexts/lib.rb +24 -0
  5. data/.contexts/yard.md +92 -0
  6. data/.github/workflows/codeql-analysis.yml +72 -0
  7. data/CHANGES.md +194 -0
  8. data/README.md +161 -90
  9. data/Rakefile +23 -19
  10. data/examples/let.rb +8 -40
  11. data/examples/mail.rb +0 -1
  12. data/examples/ones_difference.stm +0 -1
  13. data/examples/turing.rb +3 -1
  14. data/lib/tins/alias.rb +1 -0
  15. data/lib/tins/annotate.rb +37 -27
  16. data/lib/tins/ask_and_send.rb +41 -0
  17. data/lib/tins/attempt.rb +39 -0
  18. data/lib/tins/bijection.rb +34 -0
  19. data/lib/tins/case_predicate.rb +21 -0
  20. data/lib/tins/complete.rb +16 -0
  21. data/lib/tins/concern.rb +100 -0
  22. data/lib/tins/date_dummy.rb +36 -4
  23. data/lib/tins/date_time_dummy.rb +34 -2
  24. data/lib/tins/deep_dup.rb +9 -2
  25. data/lib/tins/deprecate.rb +27 -0
  26. data/lib/tins/dslkit.rb +563 -59
  27. data/lib/tins/duration.rb +160 -3
  28. data/lib/tins/expose.rb +54 -5
  29. data/lib/tins/extract_last_argument_options.rb +9 -0
  30. data/lib/tins/file_binary.rb +108 -25
  31. data/lib/tins/find.rb +114 -11
  32. data/lib/tins/generator.rb +10 -2
  33. data/lib/tins/go.rb +81 -4
  34. data/lib/tins/hash_bfs.rb +69 -0
  35. data/lib/tins/hash_symbolize_keys_recursive.rb +62 -4
  36. data/lib/tins/hash_union.rb +47 -2
  37. data/lib/tins/if_predicate.rb +31 -0
  38. data/lib/tins/implement.rb +50 -0
  39. data/lib/tins/limited.rb +105 -29
  40. data/lib/tins/lines_file.rb +81 -2
  41. data/lib/tins/lru_cache.rb +54 -17
  42. data/lib/tins/memoize.rb +86 -58
  43. data/lib/tins/method_description.rb +87 -4
  44. data/lib/tins/minimize.rb +39 -11
  45. data/lib/tins/module_group.rb +27 -2
  46. data/lib/tins/named_set.rb +20 -0
  47. data/lib/tins/null.rb +86 -15
  48. data/lib/tins/once.rb +61 -4
  49. data/lib/tins/p.rb +44 -8
  50. data/lib/tins/partial_application.rb +66 -7
  51. data/lib/tins/proc_compose.rb +58 -1
  52. data/lib/tins/proc_prelude.rb +97 -10
  53. data/lib/tins/range_plus.rb +30 -2
  54. data/lib/tins/require_maybe.rb +36 -0
  55. data/lib/tins/responding.rb +39 -0
  56. data/lib/tins/secure_write.rb +25 -5
  57. data/lib/tins/sexy_singleton.rb +46 -48
  58. data/lib/tins/string_byte_order_mark.rb +33 -2
  59. data/lib/tins/string_camelize.rb +31 -2
  60. data/lib/tins/string_named_placeholders.rb +70 -0
  61. data/lib/tins/string_underscore.rb +33 -2
  62. data/lib/tins/string_version.rb +183 -10
  63. data/lib/tins/subhash.rb +35 -10
  64. data/lib/tins/temp_io.rb +7 -0
  65. data/lib/tins/temp_io_enum.rb +19 -0
  66. data/lib/tins/terminal.rb +34 -12
  67. data/lib/tins/thread_local.rb +69 -11
  68. data/lib/tins/time_dummy.rb +47 -21
  69. data/lib/tins/to.rb +15 -0
  70. data/lib/tins/to_proc.rb +17 -4
  71. data/lib/tins/token.rb +61 -2
  72. data/lib/tins/unit.rb +288 -149
  73. data/lib/tins/version.rb +1 -1
  74. data/lib/tins/write.rb +14 -3
  75. data/lib/tins/xt/blank.rb +81 -2
  76. data/lib/tins/xt/concern.rb +51 -0
  77. data/lib/tins/xt/deep_dup.rb +4 -2
  78. data/lib/tins/xt/deprecate.rb +5 -0
  79. data/lib/tins/xt/full.rb +56 -11
  80. data/lib/tins/xt/hash_bfs.rb +7 -0
  81. data/lib/tins/xt/irb.rb +46 -2
  82. data/lib/tins/xt/method_description.rb +0 -12
  83. data/lib/tins/xt/minimize.rb +7 -0
  84. data/lib/tins/xt/named.rb +71 -16
  85. data/lib/tins/xt/proc_compose.rb +4 -0
  86. data/lib/tins/xt/secure_write.rb +0 -4
  87. data/lib/tins/xt/string.rb +1 -0
  88. data/lib/tins/xt/string_camelize.rb +4 -2
  89. data/lib/tins/xt/string_named_placeholders.rb +7 -0
  90. data/lib/tins/xt/string_underscore.rb +4 -2
  91. data/lib/tins/xt/subhash.rb +11 -0
  92. data/lib/tins/xt/time_freezer.rb +43 -6
  93. data/lib/tins/xt/write.rb +0 -4
  94. data/lib/tins/xt.rb +3 -3
  95. data/lib/tins.rb +19 -3
  96. data/tests/annotate_test.rb +0 -1
  97. data/tests/bijection_test.rb +0 -1
  98. data/tests/concern_test.rb +63 -4
  99. data/tests/date_dummy_test.rb +0 -1
  100. data/tests/date_time_dummy_test.rb +0 -1
  101. data/tests/delegate_test.rb +0 -1
  102. data/tests/deprecate_test.rb +41 -0
  103. data/tests/dslkit_test.rb +15 -1
  104. data/tests/duration_test.rb +23 -2
  105. data/tests/dynamic_scope_test.rb +0 -1
  106. data/tests/extract_last_argument_options_test.rb +0 -1
  107. data/tests/find_test.rb +0 -1
  108. data/tests/from_module_test.rb +30 -3
  109. data/tests/generator_test.rb +0 -1
  110. data/tests/go_test.rb +0 -1
  111. data/tests/hash_bfs_test.rb +34 -0
  112. data/tests/hash_symbolize_keys_recursive_test.rb +0 -1
  113. data/tests/implement_test.rb +6 -9
  114. data/tests/limited_test.rb +12 -12
  115. data/tests/lines_file_test.rb +2 -1
  116. data/tests/lru_cache_test.rb +12 -1
  117. data/tests/memoize_test.rb +0 -1
  118. data/tests/method_description_test.rb +14 -20
  119. data/tests/minimize_test.rb +0 -1
  120. data/tests/module_group_test.rb +0 -1
  121. data/tests/named_set_test.rb +0 -1
  122. data/tests/null_test.rb +0 -1
  123. data/tests/partial_application_test.rb +4 -0
  124. data/tests/proc_prelude_test.rb +1 -1
  125. data/tests/require_maybe_test.rb +0 -1
  126. data/tests/scope_test.rb +1 -2
  127. data/tests/secure_write_test.rb +6 -1
  128. data/tests/sexy_singleton_test.rb +1 -1
  129. data/tests/string_named_placeholders.rb +109 -0
  130. data/tests/string_version_test.rb +3 -1
  131. data/tests/subhash_test.rb +0 -1
  132. data/tests/test_helper.rb +4 -7
  133. data/tests/time_dummy_test.rb +0 -1
  134. data/tests/time_freezer_test.rb +1 -1
  135. data/tests/to_test.rb +6 -6
  136. data/tests/token_test.rb +0 -1
  137. data/tests/unit_test.rb +0 -1
  138. data/tins.gemspec +18 -30
  139. metadata +55 -38
  140. data/lib/tins/count_by.rb +0 -8
  141. data/lib/tins/deep_const_get.rb +0 -50
  142. data/lib/tins/timed_cache.rb +0 -51
  143. data/lib/tins/uniq_by.rb +0 -10
  144. data/lib/tins/xt/count_by.rb +0 -11
  145. data/lib/tins/xt/deep_const_get.rb +0 -7
  146. data/lib/tins/xt/uniq_by.rb +0 -15
  147. data/tests/count_by_test.rb +0 -17
  148. data/tests/deep_const_get_test.rb +0 -37
  149. data/tests/uniq_by_test.rb +0 -31
  150. /data/{COPYING → LICENSE} +0 -0
data/lib/tins/to_proc.rb CHANGED
@@ -1,12 +1,25 @@
1
1
  module Tins
2
+ # This module provides a way to convert symbols into procs using the
3
+ # __send__ method for dynamic method invocation. It was necessary before
4
+ # Ruby 1.9's built-in Symbol#to_proc functionality. You still can use
5
+ # it for strings, though.
6
+ #
7
+ # Example Usage:
8
+ # class String
9
+ # include Tins::ToProc
10
+ # end
11
+ #
12
+ # ["hello", "world"].map(&'upcase') # => ["HELLO", "WORLD"]
2
13
  module ToProc
3
- # :nocov:
14
+ # Converts a Symbol into a Proc that sends the symbol's name to its
15
+ # argument
16
+ #
17
+ # @return [ Proc ] a Proc that when called will send the symbol's name as a
18
+ # message to obj with args
4
19
  def to_proc
5
20
  lambda do |obj, *args|
6
- obj.__send__(self, *args[0..-1])
21
+ obj.__send__(self, *args)
7
22
  end
8
23
  end
9
24
  end
10
25
  end
11
-
12
- require 'tins/alias'
data/lib/tins/token.rb CHANGED
@@ -1,22 +1,78 @@
1
1
  require 'securerandom'
2
2
 
3
3
  module Tins
4
+ # A secure token generator that creates cryptographically safe strings
5
+ # using customizable alphabets and random number generators.
6
+ #
7
+ # @example Basic usage
8
+ # token = Tins::Token.new
9
+ # # => "aB3xK9mN2pQ8rS4tU6vW7yZ1"
10
+ #
11
+ # @example Custom length
12
+ # token = Tins::Token.new(length: 20)
13
+ # # => "xYz123AbC456DeF789GhI0jK"
14
+ #
15
+ # @example Custom alphabet
16
+ # token = Tins::Token.new(
17
+ # alphabet: Tins::Token::BASE16_UPPERCASE_ALPHABET,
18
+ # length: 16
19
+ # )
20
+ # # => "A1B2C3D4E5F6G7H8"
21
+ #
22
+ # @example Custom random number generator
23
+ # token = Tins::Token.new(random: Random.new(42))
24
+ # # => "xYz123AbC456DeF789GhI0jK"
4
25
  class Token < String
26
+ # Default alphabet for token generation
5
27
  DEFAULT_ALPHABET =
6
28
  "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze
7
29
 
30
+ # Base64 alphabet
8
31
  BASE64_ALPHABET =
9
32
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".freeze
10
33
 
34
+ # URL-safe base64 alphabet
11
35
  BASE64_URL_FILENAME_SAFE_ALPHABET =
12
36
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".freeze
13
37
 
38
+ # Base32 alphabet
14
39
  BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".freeze
15
40
 
41
+ # Extended hex base32 alphabet
16
42
  BASE32_EXTENDED_HEX_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUV".freeze
17
43
 
18
- BASE16_ALPHABET = "0123456789ABCDEF".freeze
44
+ # Base16 uppercase alphabet
45
+ BASE16_UPPERCASE_ALPHABET = "0123456789ABCDEF".freeze
19
46
 
47
+ # Base16 lowercase alphabet
48
+ BASE16_LOWERCASE_ALPHABET = "0123456789abcdef".freeze
49
+
50
+ # Base16 default alphabet (uppercase)
51
+ BASE16_ALPHABET = BASE16_UPPERCASE_ALPHABET
52
+
53
+ # Initializes a new Token instance with specified bit length, length,
54
+ # alphabet, and random number generator.
55
+ #
56
+ # @example Basic initialization
57
+ # token = Tins::Token.new
58
+ # # => "aB3xK9mN2pQ8rS4tU6vW7yZ1"
59
+ #
60
+ # @example Custom parameters
61
+ # token = Tins::Token.new(
62
+ # bits: 256,
63
+ # alphabet: Tins::Token::BASE32_ALPHABET
64
+ # )
65
+ #
66
+ # @param bits [Integer] the number of bits for the token (default: 128)
67
+ # @param length [Integer] the length of the token (optional, mutually exclusive with bits)
68
+ # @param alphabet [String] the alphabet to use for token generation (default: DEFAULT_ALPHABET)
69
+ # @param random [Object] the random number generator to use (default: SecureRandom)
70
+ #
71
+ # @return [Tins::Token] a new Token instance
72
+ #
73
+ # @raise [ArgumentError] if alphabet has fewer than 2 symbols
74
+ # @raise [ArgumentError] if bits is not positive when length is not specified
75
+ # @raise [ArgumentError] if length is not positive when specified
20
76
  def initialize(bits: 128, length: nil, alphabet: DEFAULT_ALPHABET, random: SecureRandom)
21
77
  alphabet.size > 1 or raise ArgumentError, 'need at least 2 symbols in alphabet'
22
78
  if length
@@ -26,11 +82,14 @@ module Tins
26
82
  length = (Math.log(1 << bits) / Math.log(alphabet.size)).ceil
27
83
  end
28
84
  self.bits = (Math.log(alphabet.size ** length) / Math.log(2)).floor
29
- token = ''
85
+ token = +''
30
86
  length.times { token << alphabet[random.random_number(alphabet.size)] }
31
87
  super token
32
88
  end
33
89
 
90
+ # The bit length of the token.
91
+ #
92
+ # @return [Integer] the number of bits of entropy in the token
34
93
  attr_accessor :bits
35
94
  end
36
95
  end
data/lib/tins/unit.rb CHANGED
@@ -1,179 +1,318 @@
1
1
  require 'strscan'
2
2
  require 'bigdecimal'
3
3
 
4
- module Tins::Unit
5
- Prefix = Struct.new(:name, :step, :multiplier, :fraction)
6
-
7
- PREFIX_LC = [
8
- '', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y',
9
- ].each_with_index.map { |n, i| Prefix.new(n.freeze, 1000, 1000 ** i, false) }.freeze
10
-
11
- PREFIX_UC = [
12
- '', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y',
13
- ].each_with_index.map { |n, i| Prefix.new(n.freeze, 1024, 1024 ** i, false) }.freeze
14
-
15
- PREFIX_F = [
16
- '', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y',
17
- ].each_with_index.map { |n, i| Prefix.new(n.freeze, 1000, 1000 ** -i, true) }.freeze
18
-
19
- class ParserError < ArgumentError; end
20
-
21
- module_function
22
-
23
- def prefixes(identifier)
24
- case identifier
25
- when :uppercase, :uc, 1024
26
- PREFIX_UC
27
- when :lowercase, :lc, 1000
28
- PREFIX_LC
29
- when :fraction, :f, 0.001
30
- PREFIX_F
31
- when Array
32
- identifier
33
- end
34
- end
4
+ module Tins
5
+ # A module for parsing and formatting unit specifications with support for
6
+ # various prefix types and unit formats.
7
+ #
8
+ # @example Basic usage
9
+ # Tins::Unit.parse('1.5 GB').round # => 1610612736
10
+ # Tins::Unit.format(1610612736, unit: 'B') # => "1.500000 GB"
11
+ # Tins::Unit.parse('1000 mb', prefix: 1000).round # => 1000000000
12
+ module Unit
13
+ # A simple data structure representing a unit prefix with name, scaling step,
14
+ # multiplier, and fraction flag.
15
+ Prefix = Struct.new(:name, :step, :multiplier, :fraction)
35
16
 
36
- def format(value, format: '%f %U', prefix: 1024, unit: ?b)
37
- prefixes = prefixes(prefix)
38
- first_prefix = prefixes.first or
39
- raise ArgumentError, 'a non-empty array of prefixes is required'
40
- if value.zero?
41
- result = format.sub('%U', unit)
42
- result %= value
43
- else
44
- prefix = prefixes[
45
- (first_prefix.fraction ? -1 : 1) * Math.log(value.abs) / Math.log(first_prefix.step)
46
- ]
47
- result = format.sub('%U', "#{prefix.name}#{unit}")
48
- result %= (value / prefix.multiplier.to_f)
49
- end
50
- end
17
+ # An array of prefix objects for lowercase decimal prefixes (k, M, G...)
18
+ # based on 1000-step increments.
19
+ PREFIX_LC = [
20
+ '', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y',
21
+ ].each_with_index.map { |n, i| Prefix.new(n.freeze, 1000, 1000 ** i, false) }.freeze
51
22
 
52
- class UnitParser < StringScanner
53
- NUMBER = /([+-]?
54
- (?:0|[1-9]\d*)
55
- (?:
56
- \.\d+(?i:e[+-]?\d+) |
57
- \.\d+ |
58
- (?i:e[+-]?\d+)
59
- )?
60
- )/x
61
-
62
- def initialize(source, unit, prefixes = nil)
63
- super source
64
- if prefixes
65
- @unit_re = unit_re(Tins::Unit.prefixes(prefixes), unit)
66
- @unit_lc_re = @unit_uc_re = nil
67
- else
68
- @unit_lc_re = unit_re(Tins::Unit.prefixes(:lc), unit)
69
- @unit_uc_re = unit_re(Tins::Unit.prefixes(:uc), unit)
70
- @unit_re = nil
23
+ # An array of prefix objects for uppercase binary prefixes (K, M, G...) based
24
+ # on 1024-step increments.
25
+ PREFIX_UC = [
26
+ '', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y',
27
+ ].each_with_index.map { |n, i| Prefix.new(n.freeze, 1024, 1024 ** i, false) }.freeze
28
+
29
+ # An array of prefix objects for fractional prefixes (m, µ, n...) based on
30
+ # 1000-step decrements.
31
+ PREFIX_F = [
32
+ '', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y',
33
+ ].each_with_index.map { |n, i| Prefix.new(n.freeze, 1000, 1000 ** -i, true) }.freeze
34
+
35
+ # A custom exception class for parser errors that inherits from ArgumentError
36
+ class ParserError < ArgumentError; end
37
+
38
+ module_function
39
+
40
+ # The prefixes method returns an array of prefix objects based on the given
41
+ # identifier.
42
+ #
43
+ # This method maps different identifier symbols and values to predefined
44
+ # arrays of prefix objects, allowing for flexible configuration of unit
45
+ # prefixes.
46
+ #
47
+ # @param identifier [Symbol, Integer, Array] the identifier specifying which
48
+ # prefix set to return
49
+ # @return [Array] an array of prefix objects corresponding to the identifier
50
+ def prefixes(identifier)
51
+ case identifier
52
+ when :uppercase, :uc, 1024
53
+ PREFIX_UC
54
+ when :lowercase, :lc, 1000
55
+ PREFIX_LC
56
+ when :fraction, :f, 0.001
57
+ PREFIX_F
58
+ when Array
59
+ identifier
71
60
  end
72
- @number = 1.0
73
61
  end
74
62
 
75
- def unit_re(prefixes, unit)
76
- re = Regexp.new(
77
- "(#{prefixes.reverse.map { |pre| Regexp.quote(pre.name) } * ?|})(#{unit})"
78
- )
79
- re.singleton_class.class_eval do
80
- define_method(:prefixes) { prefixes }
63
+ # Format a value using unit prefixes and a specified format template.
64
+ #
65
+ # This method takes a numerical value and formats it according to the given
66
+ # format string, inserting the appropriate unit prefix based on the specified
67
+ # prefix type and unit identifier.
68
+ #
69
+ # @param value [Numeric] the numerical value to format
70
+ # @param format [String] the format template to use for output, default is '%f %U'
71
+ # @param prefix [Object] the prefix configuration to use, default is 1024 (binary prefixes)
72
+ # @param unit [String, Symbol] the unit identifier to append, default is ?b (bytes)
73
+ def format(value, format: '%f %U', prefix: 1024, unit: ?b)
74
+ prefixes = prefixes(prefix)
75
+ first_prefix = prefixes.first or
76
+ raise ArgumentError, 'a non-empty array of prefixes is required'
77
+ if value.zero?
78
+ result = format.sub('%U', unit)
79
+ result %= value
80
+ else
81
+ prefix = prefixes[
82
+ (first_prefix.fraction ? -1 : 1) * Math.log(value.abs) / Math.log(first_prefix.step)
83
+ ]
84
+ result = format.sub('%U', "#{prefix.name}#{unit}")
85
+ result %= (value / prefix.multiplier.to_f)
81
86
  end
82
- re
83
87
  end
84
88
 
85
- private :unit_re
89
+ # A parser for unit specifications that extends StringScanner
90
+ #
91
+ # This class is responsible for parsing strings that contain numerical values
92
+ # followed by unit specifications, supporting various prefix types and unit
93
+ # formats for flexible unit parsing.
94
+ class UnitParser < StringScanner
95
+ # A regular expression matching a number.
96
+ NUMBER = /([+-]?
97
+ (?:0|[1-9]\d*)
98
+ (?:
99
+ \.\d+(?i:e[+-]?\d+) |
100
+ \.\d+ |
101
+ (?i:e[+-]?\d+)
102
+ )?
103
+ )/x
86
104
 
87
- attr_reader :number
105
+ # The initialize method sets up a new UnitParser instance with the given
106
+ # source string, unit identifier, and optional prefixes configuration.
107
+ #
108
+ # @param source [String] the input string to parse for units
109
+ # @param unit [String, Symbol] the unit identifier to look for in the source
110
+ # @param prefixes [Object, nil] optional prefixes configuration (can be array or symbol)
111
+ # @return [UnitParser] a new UnitParser instance configured with the provided parameters
112
+ def initialize(source, unit, prefixes = nil)
113
+ super source
114
+ if prefixes
115
+ @unit_re = unit_re(Tins::Unit.prefixes(prefixes), unit)
116
+ @unit_lc_re = @unit_uc_re = nil
117
+ else
118
+ @unit_lc_re = unit_re(Tins::Unit.prefixes(:lc), unit)
119
+ @unit_uc_re = unit_re(Tins::Unit.prefixes(:uc), unit)
120
+ @unit_re = nil
121
+ end
122
+ @number = 1.0
123
+ end
88
124
 
89
- def scan(re)
90
- re.nil? and return
91
- super
92
- end
125
+ # The unit_re method creates a regular expression for matching units with
126
+ # prefixes
127
+ #
128
+ # This method constructs a regular expression pattern that can match unit
129
+ # strings including their associated prefixes, which is useful for parsing
130
+ # formatted unit specifications in text.
131
+ #
132
+ # @param prefixes [Array] an array of prefix objects with name attributes
133
+ # @param unit [String] the base unit string to match against
134
+ #
135
+ # @return [Regexp] a regular expression object that can match prefixed units
136
+ def unit_re(prefixes, unit)
137
+ re = Regexp.new(
138
+ "(#{prefixes.reverse.map { |pre| Regexp.quote(pre.name) } * ?|})(#{unit})"
139
+ )
140
+ re.singleton_class.class_eval do
141
+ define_method(:prefixes) { prefixes }
142
+ end
143
+ re
144
+ end
93
145
 
94
- def scan_number
95
- scan(NUMBER) or return
96
- @number *= BigDecimal(self[1])
97
- end
146
+ private :unit_re
98
147
 
99
- def scan_unit
100
- case
101
- when scan(@unit_re)
102
- prefix = @unit_re.prefixes.find { |pre| pre.name == self[1] } or return
103
- @number *= prefix.multiplier
104
- when scan(@unit_lc_re)
105
- prefix = @unit_lc_re.prefixes.find { |pre| pre.name == self[1] } or return
106
- @number *= prefix.multiplier
107
- when scan(@unit_uc_re)
108
- prefix = @unit_uc_re.prefixes.find { |pre| pre.name == self[1] } or return
109
- @number *= prefix.multiplier
110
- end
111
- end
148
+ # The number reader method returns the value of the number attribute.
149
+ #
150
+ # @return [Object] the current value of the number attribute
151
+ attr_reader :number
112
152
 
113
- def scan_char(char)
114
- scan(/#{char}/) or return
115
- end
153
+ # The scan method scans a regular expression against the current string
154
+ # scanner state.
155
+ #
156
+ # This method delegates to the parent StringScanner's scan method, but
157
+ # includes a guard clause to return early if the provided regular
158
+ # expression is nil.
159
+ #
160
+ # @param re [Regexp, nil] the regular expression to scan for
161
+ # @return [String, nil] the matched string if found, or nil if no match
162
+ def scan(re)
163
+ re.nil? and return
164
+ super
165
+ end
116
166
 
117
- def parse
118
- raise NotImplementedError
119
- end
120
- end
167
+ # The scan_number method parses a number from the current string scanner
168
+ # state and multiplies the internal number value by it.
169
+ def scan_number
170
+ scan(NUMBER) or return
171
+ @number *= BigDecimal(self[1])
172
+ end
121
173
 
122
- class FormatParser < StringScanner
123
- def initialize(format, unit_parser)
124
- super format
125
- @unit_parser = unit_parser
126
- end
174
+ # The scan_unit method attempts to match unit patterns against the current
175
+ # string and updates the internal number value by multiplying it with the
176
+ # corresponding prefix multiplier
177
+ def scan_unit
178
+ case
179
+ when scan(@unit_re)
180
+ prefix = @unit_re.prefixes.find { |pre| pre.name == self[1] } or return
181
+ @number *= prefix.multiplier
182
+ when scan(@unit_lc_re)
183
+ prefix = @unit_lc_re.prefixes.find { |pre| pre.name == self[1] } or return
184
+ @number *= prefix.multiplier
185
+ when scan(@unit_uc_re)
186
+ prefix = @unit_uc_re.prefixes.find { |pre| pre.name == self[1] } or return
187
+ @number *= prefix.multiplier
188
+ end
189
+ end
127
190
 
128
- def reset
129
- super
130
- @unit_parser.reset
131
- end
191
+ # The scan_char method attempts to match a character pattern against the
192
+ # current string scanner state.
193
+ #
194
+ # @param char [String] the character to scan for
195
+ # @return [String, nil] the matched string if found, or nil if no match
196
+ def scan_char(char)
197
+ scan(/#{char}/) or return
198
+ end
132
199
 
133
- def location
134
- @unit_parser.peek(10).inspect
200
+ # The parse method is intended to be overridden by subclasses to provide
201
+ # specific parsing functionality.
202
+ #
203
+ # @return [Object] the parsed result
204
+ def parse
205
+ raise NotImplementedError
206
+ end
135
207
  end
136
208
 
137
- private :location
138
-
139
- def parse
140
- reset
141
- until eos? || @unit_parser.eos?
142
- case
143
- when scan(/%f/)
144
- @unit_parser.scan_number or
145
- raise ParserError, "\"%f\" expected at #{location}"
146
- when scan(/%U/)
147
- @unit_parser.scan_unit or
148
- raise ParserError, "\"%U\" expected at #{location}"
149
- when scan(/%%/)
150
- @unit_parser.scan_char(?%) or
151
- raise ParserError, "#{?%.inspect} expected at #{location}"
152
- else
153
- char = scan(/./)
154
- @unit_parser.scan_char(char) or
155
- raise ParserError, "#{char.inspect} expected at #{location}"
156
- end
209
+ # A parser for unit specifications that extends StringScanner
210
+ #
211
+ # This class is responsible for parsing strings that contain numerical values
212
+ # followed by unit specifications, supporting various prefix types and unit
213
+ # formats for flexible unit parsing.
214
+ class FormatParser < StringScanner
215
+
216
+ # The initialize method sets up a new UnitParser instance with the given
217
+ # format and unit parser.
218
+ #
219
+ # @param format [String] the format string to use for parsing
220
+ # @param unit_parser [Tins::Unit::UnitParser] the unit parser to use for
221
+ # parsing units
222
+ # @return [Tins::Unit::FormatParser] a new FormatParser instance configured
223
+ # with the provided parameters
224
+ def initialize(format, unit_parser)
225
+ super format
226
+ @unit_parser = unit_parser
227
+ end
228
+
229
+ # The reset method resets the unit parser state.
230
+ #
231
+ # This method calls the superclass reset implementation and then resets
232
+ # the internal unit parser instance to its initial state.
233
+ def reset
234
+ super
235
+ @unit_parser.reset
236
+ end
237
+
238
+ # The location method returns a string representation of the current
239
+ # parsing position by peeking at the next 10 characters from the unit
240
+ # parser and inspecting them
241
+ # @return [String] the inspected representation of the next 10 characters from the parser
242
+ # @private
243
+ def location
244
+ @unit_parser.peek(10).inspect
157
245
  end
158
- unless eos? && @unit_parser.eos?
159
- raise ParserError,
160
- "format #{string.inspect} and string "\
246
+
247
+ private :location
248
+
249
+ # The parse method parses a format string using a unit parser and returns
250
+ # the parsed number.
251
+ #
252
+ # This method processes a format template by scanning for specific pattern directives
253
+ # (%f for numbers, %U for units, %% for literal percent signs) and validates that
254
+ # the input string matches the expected format. It handles parsing errors by raising
255
+ # ParserError exceptions with descriptive messages about mismatches.
256
+ #
257
+ # @return [Float] the parsed numerical value with units applied
258
+ # @raise [ParserError] if the format string or input string doesn't match the expected pattern
259
+ # @raise [ParserError] if a required number or unit is missing at a specific location
260
+ # @raise [ParserError] if literal percent signs don't match expected positions
261
+ def parse
262
+ reset
263
+ until eos? || @unit_parser.eos?
264
+ case
265
+ when scan(/%f/)
266
+ @unit_parser.scan_number or
267
+ raise ParserError, "\"%f\" expected at #{location}"
268
+ when scan(/%U/)
269
+ @unit_parser.scan_unit or
270
+ raise ParserError, "\"%U\" expected at #{location}"
271
+ when scan(/%%/)
272
+ @unit_parser.scan_char(?%) or
273
+ raise ParserError, "#{?%.inspect} expected at #{location}"
274
+ else
275
+ char = scan(/./)
276
+ @unit_parser.scan_char(char) or
277
+ raise ParserError, "#{char.inspect} expected at #{location}"
278
+ end
279
+ end
280
+ unless eos? && @unit_parser.eos?
281
+ raise ParserError,
282
+ "format #{string.inspect} and string "\
161
283
  "#{@unit_parser.string.inspect} do not match"
284
+ end
285
+ @unit_parser.number
162
286
  end
163
- @unit_parser.number
164
287
  end
165
- end
166
288
 
167
- # Parse the string +string+ if it matches +format+ with the unit +unit+ and
168
- # the prefixes specified by +prefix+.
169
- def parse(string, format: '%f %U', unit: ?b, prefix: nil)
170
- prefixes = prefixes(prefix)
171
- FormatParser.new(format, UnitParser.new(string, unit, prefixes)).parse
172
- end
289
+ # Parse the string using the specified format and unit information
290
+ #
291
+ # This method takes a string and parses it according to a given format template,
292
+ # extracting numerical values and their associated units. It supports various
293
+ # prefix types and unit specifications for flexible parsing.
294
+ #
295
+ # @param string [String] the input string to parse
296
+ # @param format [String] the format template to use for parsing (default: '%f %U')
297
+ # @param unit [String, Symbol] the unit identifier to use (default: ?b)
298
+ # @param prefix [Object] the prefix configuration to use (default: nil)
299
+ #
300
+ # @return [Object] the parsed result based on the format and unit specifications
301
+ def parse(string, format: '%f %U', unit: ?b, prefix: nil)
302
+ prefixes = prefixes(prefix)
303
+ FormatParser.new(format, UnitParser.new(string, unit, prefixes)).parse
304
+ end
173
305
 
174
- def parse?(string, **options)
175
- parse(string, **options)
176
- rescue ParserError
177
- nil
306
+ # The parse? method attempts to parse a string using the specified options
307
+ # and returns nil if parsing fails.
308
+ #
309
+ # @param string [String] the string to parse
310
+ # @param options [Hash] the options to pass to the parse method
311
+ # @return [Object, nil] the parsed result or nil if parsing fails
312
+ def parse?(string, **options)
313
+ parse(string, **options)
314
+ rescue ParserError
315
+ nil
316
+ end
178
317
  end
179
318
  end
data/lib/tins/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Tins
2
2
  # Tins version
3
- VERSION = '1.32.0'
3
+ VERSION = '1.44.0'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/lib/tins/write.rb CHANGED
@@ -1,11 +1,24 @@
1
1
  require 'tins/secure_write'
2
2
 
3
3
  module Tins
4
+ # Tins::Write provides secure write functionality that can be extended onto
5
+ # modules/classes to add a `write` method.
6
+ #
7
+ # When a module is extended with Tins::Write, it will:
8
+ # - Extend the module with SecureWrite methods
9
+ # - Conditionally alias `secure_write` to `write` if no existing `write` method exists
10
+ # - Issue a warning if `write` already exists (when $DEBUG is enabled)
4
11
  module Write
12
+ # Called when Tins::Write is extended onto a module.
13
+ # Extends the receiving module with SecureWrite functionality
14
+ # and conditionally aliases secure_write to write.
15
+ #
16
+ # @param modul [Module] The module being extended
5
17
  def self.extended(modul)
6
18
  modul.extend SecureWrite
7
19
  if modul.respond_to?(:write)
8
- $DEBUG and warn "Skipping inclusion of Tins::Write#write method, include Tins::Write::SecureWrite#secure_write instead"
20
+ $DEBUG and warn "Skipping inclusion of Tins::Write#write method, "\
21
+ "include Tins::Write::SecureWrite#secure_write instead"
9
22
  else
10
23
  class << modul; self; end.instance_eval do
11
24
  alias_method :write, :secure_write
@@ -15,5 +28,3 @@ module Tins
15
28
  end
16
29
  end
17
30
  end
18
-
19
- require 'tins/alias'