fstrings 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7380d3cac8fb3aefc682a76ad4494ab1d073ce0d9151282cd308da13e4d816e3
4
+ data.tar.gz: 2c68d89360ab486f60afac7b26282f1cd16fb4039d507ec586ce78906fa7c786
5
+ SHA512:
6
+ metadata.gz: 73ca4f916f6ce4da769ca3e4bf4abe44b7b4245e56ab9654bc2798a43e30967fc9c4f6fcec0b0c23992fab62bd6c6420e15e85a1c852e7c61d64d2071bf8374b
7
+ data.tar.gz: 303bd05300946f90da31bb282cac0e08221387e81d907320b997a98205701d3bc1c6526cd8cdcf588577ae94173d87fb32bfe51d5e13da80f28bc0b7628a4d0a
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Victor 'Zverok' Shepelev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # FStrings
2
+
3
+ FStrings is an _experimental_ gem implementing Python-alike fstrings (formatting strings) in Ruby.
4
+
5
+ The idea is, in Ruby, we have two ways to insert some variable values in strings:
6
+
7
+ 1. String interpolation: `puts "Foo #{value} bar"`
8
+ 2. `String#%` (or `Kernel#format`, if you want): `puts "Foo %.2f value" % value`
9
+
10
+ First is more convenient (with the variable name where it should be rendered), while the second is much more powerful, allowing to specify various formatting flags. `FStrings` tries to close this gap, with a bit of idea stealing (from the [Python](https://www.python.org/dev/peps/pep-0498/)) and a bit of dark magic ([binding_of_caller](http://github.com/banister/binding_of_caller)).
11
+
12
+ ## Showcase
13
+
14
+ In its basic form, FStrings formatting looks just like string interpolation (just using `{}` instead of `#{}`):
15
+
16
+ ```ruby
17
+ require 'fstrings'
18
+ include FStrings
19
+
20
+ value = 5
21
+ puts f"Simple: {value}"
22
+ # => "Simple: 5"
23
+ ```
24
+
25
+ But it also allows to specify formatting flags, after `%` sign (the regular [Kernel#format](https://ruby-doc.org/core-2.7.0/Kernel.html#method-i-format)'s syntax works):
26
+
27
+ ```ruby
28
+ puts f"Formatted: {value%+i}"
29
+ # => "Formatted: +5"
30
+
31
+ float = 1.2345
32
+ puts f"Formatted: {float%.2f}"
33
+ # => "Formatted: 1.23"
34
+ ```
35
+
36
+ That's mostly it! But not **all** of it :)
37
+
38
+ FStrings also support **`x=` syntax** (borrowed from the recent [Python 3.8](https://docs.python.org/3/whatsnew/3.8.html#f-strings-support-for-self-documenting-expressions-and-debugging)), indispensable for `puts`-debugging:
39
+
40
+ ```ruby
41
+ puts f"Named: {value=%+i}"
42
+ # => "Named: value=+5"
43
+
44
+ # Any expression can be interpolated this way:
45
+ r = 12
46
+ puts f"Circle area: {Math::PI * r**2 = %.3f}"
47
+ # => "Circle area: Math::PI * r**2 = 452.389"
48
+ ```
49
+
50
+ FStrings allows to define **custom formatters** for your own classes, and automatically define one for `Time` (it passes the format string to `strftime`):
51
+
52
+ ```ruby
53
+ puts f"Current time is {Time.now %H:%M (%b %d)}"
54
+ # => "Current time is 15:00 (Jan 07)"
55
+ ```
56
+
57
+ To define your own, just do this:
58
+
59
+ ```ruby
60
+ Point = Struct.new(:x, :y)
61
+ # First argument is formatted value, second is format string
62
+ FStrings.def_formatter(Point) { |val, str| str.gsub('%x', val.x.to_s).gsub('%y', val.y.to_s) }
63
+
64
+ point = Point.new(10, 20)
65
+
66
+ puts f"See, it works: {point %x;%y}"
67
+ # => "See, it works: 10;20"
68
+ ```
69
+
70
+ (The formatting strings considered everything starting from the first `%` including it.)
71
+
72
+ ## Quirks and problems
73
+
74
+ The library is _new and experimental_. It is _probably_ helpful in debugging, but probably not advised for any production. The problems I can think of:
75
+
76
+ * `binding_of_caller` and `eval` are used inside. It is black and unholy magic, obviously;
77
+ * funny `f"foo"` syntax is used to make it look like Python's native fstrings, which could be repulsive for some. In fact, it is just `f()` method, you can use it without `include FStrings`, with just `FStrings.f()`;
78
+ * fstrings-parser is not that mature; it is tested, but can break on more complicated strings (which you hopefully won't need for debugging);
79
+ * probably, parsed strings should be cached, currently, they are not (so in a method which you call 2 mln times, could provide serious slowdown);
80
+ * considering simplistic formatting string definition, statements using `%` can't be inspected (everything after `%` would be thought to be a formatting string).
81
+
82
+ ## Author & license
83
+
84
+ * [Victor Shepelev](https://zverok.github.io)
85
+ * MIT.
data/lib/fstrings.rb ADDED
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'binding_of_caller'
4
+ require 'strscan'
5
+
6
+ require_relative 'fstrings/formats'
7
+ require_relative 'fstrings/parser'
8
+
9
+ # Python-alike fstrings (formatting strings) with a Ruby flavour.
10
+ #
11
+ # @example
12
+ # include FStrings
13
+ #
14
+ # i = 1
15
+ # f = 1.23
16
+ #
17
+ # # Basic form: Just like #{}
18
+ # f"Simple! {i}"
19
+ # # => "Simple! 1"
20
+ #
21
+ # # Inline formatting is supported, same flags as Kernel#format
22
+ # f"Look: {i%+i}"
23
+ # # => "Look: +1"
24
+ # f"Floats... {f%.1f}"
25
+ # # => "Floats... 1.2"
26
+ #
27
+ # # Any statement is supported:
28
+ # f"The whole statement: {i + f %.1f}"
29
+ # # => "The whole statement: 2.2"
30
+ #
31
+ # # = at the end of statement handy for debugging:
32
+ # f"Variable names, too! {i + f = %.1f}"
33
+ # # => "Variable names, too! i + f = 2.2"
34
+ #
35
+ # # Time and date formatting is supported:
36
+ # f"Currently, it is {Time.now %H:%M (at %b %d)}"
37
+ # # => "Currently, it is 14:31 (at Jan 07)"
38
+ #
39
+ # # Custom object formatting definition is supported:
40
+ # Point = Struct.new(:x, :y)
41
+ # FStrings.def_formatter(Point) { |val, fmtstring| fmtstring % [val.x, val.y] }
42
+ # f"The point: [{p= %.1f;%.1f}]"
43
+ # # => "The point: [p= 1.3;2.5]"
44
+ #
45
+ module FStrings
46
+ # Main library's interface. See {FStrings} main docs for examples.
47
+ # @param string [String] Formatting string
48
+ # @return String
49
+ def f(string)
50
+ # TODO: cache str2code results?
51
+ binding.of_caller(1).eval(Parser.str2code(string))
52
+ end
53
+
54
+ extend self
55
+
56
+ # Define custom formatters for user classes. Formatting block should accept `value` of
57
+ # specified class, and formatting `string`, and return string.
58
+ #
59
+ # See main {FStrings} docs for a (simplistic) example of usage.
60
+ #
61
+ # @param klass [Module] Type of values the formatter is defined for.
62
+ # @yield value Of the specified `klass`
63
+ # @yield fmtstring [String] What was after `%` sign in the fstring (including `%` itself)
64
+ # @yieldreturn String
65
+ #
66
+ def self.def_formatter(klass, &formatter)
67
+ Formats[klass] = formatter
68
+ end
69
+
70
+ def_formatter(Object) { |val, string| string % val }
71
+ def_formatter(Time, &:strftime)
72
+ require 'date'
73
+ def_formatter(Date, &:strftime)
74
+ def_formatter(DateTime, &:strftime)
75
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FStrings
4
+ # @private
5
+ module Formats
6
+ class << self
7
+ def formats
8
+ @formats ||= {}
9
+ end
10
+
11
+ def []=(klass, formatter)
12
+ formats[klass] = formatter
13
+ end
14
+
15
+ def for(klass)
16
+ formats.select { |k,| klass <= k }.min_by { |k,| klass.ancestors.index(k) }.last
17
+ end
18
+
19
+ def apply(val, format)
20
+ self.for(val.class).call(val, format)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FStrings
4
+ # @private
5
+ module Parser
6
+ using(Module.new do
7
+ refine String do
8
+ def to_proc
9
+ method(:%).to_proc
10
+ end
11
+ end
12
+ end)
13
+
14
+ class << self
15
+ def str2code(string)
16
+ res = []
17
+ scan = StringScanner.new(string)
18
+ until scan.eos?
19
+ res << scan_simple(scan).inspect
20
+ res << statement2code(scan_statement(scan)) unless scan.eos?
21
+ end
22
+ res.join(' + ')
23
+ end
24
+
25
+ private
26
+
27
+ def scan_simple(scan)
28
+ str = scan.scan_until(/\{|$/)
29
+ if scan.peek(1) == '{'
30
+ str + scan.scan(/\{/) + scan_simple(scan)
31
+ else
32
+ str.sub(/\{$/, '')
33
+ end
34
+ end
35
+
36
+ def scan_statement(scan)
37
+ expr, char = scan.scan_until(/[}%]/).then { |str| [str[0...-1], str[-1]] }
38
+ # fmt will include the first %-char which also signifies it
39
+ fmt, = scan.scan_until(/\}/).then { |str| [str[0...-1], str[-1]] } if char == '%'
40
+ if expr.match?(/\s*=\s*$/)
41
+ prefix = expr
42
+ expr = expr.sub(/\s*=\s*$/, '')
43
+ end
44
+ {expr: expr.strip, fmt: fmt&.then(&'%%%s'), prefix: prefix}
45
+ end
46
+
47
+ def statement2code(expr:, fmt:, prefix:)
48
+ [
49
+ prefix&.then(&'%p +'),
50
+ fmt&.then { 'FStrings::Formats.apply(' },
51
+ '(',
52
+ expr,
53
+ ')',
54
+ fmt&.then(&', %p)'),
55
+ '.to_s'
56
+ ].compact.join
57
+ end
58
+ end
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fstrings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Victor Shepelev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: binding_of_caller
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-its
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: saharspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.0.6
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.0.6
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubygems-tasks
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: yard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: ' Python-alike formatting strings with Ruby flavour: f"{x=%.2f}"
154
+
155
+ '
156
+ email: zverok.offline@gmail.com
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - LICENSE.txt
162
+ - README.md
163
+ - lib/fstrings.rb
164
+ - lib/fstrings/formats.rb
165
+ - lib/fstrings/parser.rb
166
+ homepage: https://github.com/zverok/fstrings
167
+ licenses:
168
+ - MIT
169
+ metadata: {}
170
+ post_install_message:
171
+ rdoc_options: []
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: 2.4.0
179
+ required_rubygems_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubygems_version: 3.0.3
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: Python-alike fstrings (formatting strings) for Ruby
189
+ test_files: []