fstrings 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/lib/fstrings.rb +75 -0
- data/lib/fstrings/formats.rb +24 -0
- data/lib/fstrings/parser.rb +60 -0
- metadata +189 -0
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: []
|