plumb 0.0.4 → 0.0.5
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 +4 -4
- data/README.md +223 -10
- data/bench/compare_parametric_schema.rb +102 -0
- data/bench/compare_parametric_struct.rb +68 -0
- data/bench/parametric_schema.rb +229 -0
- data/bench/plumb_hash.rb +109 -0
- data/examples/event_registry.rb +34 -27
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/attributes.rb +13 -7
- data/lib/plumb/composable.rb +123 -4
- data/lib/plumb/json_schema_visitor.rb +11 -2
- data/lib/plumb/match_class.rb +1 -1
- data/lib/plumb/pipeline.rb +21 -2
- data/lib/plumb/tagged_hash.rb +1 -1
- data/lib/plumb/types.rb +24 -0
- data/lib/plumb/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 896f2ec4a63cde86dd22aaec579696e19c980b09c09e4a4d9d4690f9505f742d
|
4
|
+
data.tar.gz: 3296480dd0026e8050b624e3e1d65a020fde376a87c4ba269c6481b57c40d19e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76ecebbe1dc408630107170a213d0145af08bc8d9cec3ef28d66f8aa1eb3fee09352ae89c00aed1eb11b3a52709100f65e6f0a2a43b4a38ed777709f096f9198
|
7
|
+
data.tar.gz: 0cdbf9c24900fdbcd8393dc4f1d260063b303cc44cf84caa86bdb4d34a6edf77e1f75e431481121b9004c7fbac695f3021b4a7be965c34c1d4b96146f17c2c4c
|
data/README.md
CHANGED
@@ -10,6 +10,8 @@ If you're after raw performance and versatility I strongly recommend you use the
|
|
10
10
|
|
11
11
|
For a description of the core architecture you can read [this article](https://ismaelcelis.com/posts/composable-pipelines-in-ruby/).
|
12
12
|
|
13
|
+
Some use cases in the [examples directory](https://github.com/ismasan/plumb/tree/main/examples)
|
14
|
+
|
13
15
|
## Installation
|
14
16
|
|
15
17
|
Install in your environment with `gem install plumb`, or in your `Gemfile` with
|
@@ -58,7 +60,7 @@ module Types
|
|
58
60
|
end
|
59
61
|
|
60
62
|
Types::String.parse("hello") # => "hello"
|
61
|
-
Types::String.parse(10) # raises "Must be a String" (Plumb::
|
63
|
+
Types::String.parse(10) # raises "Must be a String" (Plumb::ParseError)
|
62
64
|
```
|
63
65
|
|
64
66
|
Plumb ships with basic types already defined, such as `Types::String` and `Types::Integer`. See the full list below.
|
@@ -75,7 +77,7 @@ Email.parse('hello@server.com') # 'hello@server.com'
|
|
75
77
|
# Or a Range
|
76
78
|
AdultAge = Types::Integer[18..]
|
77
79
|
AdultAge.parse(20) # 20
|
78
|
-
AdultAge.parse(17) # raises "Must be within 18.."" (Plumb::
|
80
|
+
AdultAge.parse(17) # raises "Must be within 18.."" (Plumb::ParseError)
|
79
81
|
|
80
82
|
# Or literal values
|
81
83
|
Twenty = Types::Integer[20]
|
@@ -113,7 +115,7 @@ result.errors # 'must be an Integer'
|
|
113
115
|
|
114
116
|
```ruby
|
115
117
|
Types::Integer.parse(10) # 10
|
116
|
-
Types::Integer.parse('10') # raises Plumb::
|
118
|
+
Types::Integer.parse('10') # raises Plumb::ParseError
|
117
119
|
```
|
118
120
|
|
119
121
|
|
@@ -161,7 +163,7 @@ In other words, `A >> B` means "if A succeeds, pass its result to B. Otherwise r
|
|
161
163
|
StringOrInt = Types::String | Types::Integer
|
162
164
|
StringOrInt.parse('hello') # "hello"
|
163
165
|
StringOrInt.parse(10) # 10
|
164
|
-
StringOrInt.parse({}) # raises Plumb::
|
166
|
+
StringOrInt.parse({}) # raises Plumb::ParseError
|
165
167
|
```
|
166
168
|
|
167
169
|
Custom default value logic for non-emails
|
@@ -230,6 +232,9 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
|
|
230
232
|
* `Types::Numeric`
|
231
233
|
* `Types::String`
|
232
234
|
* `Types::Hash`
|
235
|
+
* `Types::UUID::V4`
|
236
|
+
* `Types::Email`
|
237
|
+
* `Types::Date`
|
233
238
|
* `Types::Lax::Integer`
|
234
239
|
* `Types::Lax::String`
|
235
240
|
* `Types::Lax::Symbol`
|
@@ -237,8 +242,9 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
|
|
237
242
|
* `Types::Forms::Nil`
|
238
243
|
* `Types::Forms::True`
|
239
244
|
* `Types::Forms::False`
|
245
|
+
* `Types::Forms::Date`
|
240
246
|
|
241
|
-
TODO:
|
247
|
+
TODO: datetime, others.
|
242
248
|
|
243
249
|
### Policies
|
244
250
|
|
@@ -261,7 +267,7 @@ Allow `nil` values.
|
|
261
267
|
nullable_str = Types::String.nullable
|
262
268
|
nullable_srt.parse(nil) # nil
|
263
269
|
nullable_str.parse('hello') # 'hello'
|
264
|
-
nullable_str.parse(10) #
|
270
|
+
nullable_str.parse(10) # ParseError
|
265
271
|
```
|
266
272
|
|
267
273
|
Note that this just encapsulates the following composition:
|
@@ -522,7 +528,52 @@ CSVLine = Types::String.split(/\s*;\s*/)
|
|
522
528
|
CSVLine.parse('a;b;c') # => ['a', 'b', 'c']
|
523
529
|
```
|
524
530
|
|
531
|
+
#### `:rescue`
|
532
|
+
|
533
|
+
Wraps a step's execution, rescues a specific exception and returns an invalid result.
|
534
|
+
|
535
|
+
Useful for turning a 3rd party library's exception into an invalid result that plays well with Plumb's type compositions.
|
536
|
+
|
537
|
+
Example: this is how `Types::Forms::Date` uses the `:rescue` policy to parse strings with `Date.parse` and turn `Date::Error` exceptions into Plumb errors.
|
538
|
+
|
539
|
+
```ruby
|
540
|
+
# Accept a string that can be parsed into a Date
|
541
|
+
# via Date.parse
|
542
|
+
# If Date.parse raises a Date::Error, return a Result::Invalid with
|
543
|
+
# the exception's message as error message.
|
544
|
+
type = Types::String
|
545
|
+
.build(::Date, :parse)
|
546
|
+
.policy(:rescue, ::Date::Error)
|
547
|
+
|
548
|
+
type.resolve('2024-02-02') # => Result::Valid with Date object
|
549
|
+
type.resolve('2024-') # => Result::Invalid with error message
|
550
|
+
```
|
551
|
+
|
552
|
+
### `Types::Interface`
|
553
|
+
|
554
|
+
Use this for objects that must respond to one or more methods.
|
555
|
+
|
556
|
+
```ruby
|
557
|
+
Iterable = Types::Interface[:each, :map]
|
558
|
+
Iterable.parse([1,2,3]) # => [1,2,3]
|
559
|
+
Iterable.parse(10) # => raises error
|
560
|
+
```
|
561
|
+
|
562
|
+
This can be useful combined with `case` statements, too:
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
value = [1,2,3]
|
566
|
+
case value
|
567
|
+
when Iterable
|
568
|
+
# do something with array
|
569
|
+
when Stringable
|
570
|
+
# do something with string
|
571
|
+
when Readable
|
572
|
+
# do something with IO or similar
|
573
|
+
end
|
574
|
+
```
|
525
575
|
|
576
|
+
TODO: make this a bit more advanced. Check for method arity.
|
526
577
|
|
527
578
|
### `Types::Hash`
|
528
579
|
|
@@ -904,6 +955,13 @@ person.valid? # false
|
|
904
955
|
person.errors[:age] # 'must be an integer'
|
905
956
|
```
|
906
957
|
|
958
|
+
Data structs can also be defined from `Types::Hash` instances.
|
959
|
+
|
960
|
+
```ruby
|
961
|
+
PersonHash = Types::Hash[name: String, age?: Integer]
|
962
|
+
PersonStruct = Types::Data[PersonHash]
|
963
|
+
```
|
964
|
+
|
907
965
|
#### `#with`
|
908
966
|
|
909
967
|
Note that these instances cannot be mutated (there's no attribute setters), but they can be copied with partial attributes with `#with`
|
@@ -1062,13 +1120,168 @@ person.friend.name # 'joan'
|
|
1062
1120
|
person.friend.friend # nil
|
1063
1121
|
```
|
1064
1122
|
|
1123
|
+
### Plumb::Pipeline
|
1065
1124
|
|
1125
|
+
`Plumb::Pipeline` offers a sequential, step-by-step syntax for composing processing steps, as well as a simple middleware API to wrap steps for metrics, logging, debugging, caching and more. See the [command objects](https://github.com/ismasan/plumb/blob/main/examples/command_objects.rb) example for a worked use case.
|
1066
1126
|
|
1067
|
-
|
1127
|
+
#### `#pipeline` helper
|
1068
1128
|
|
1069
|
-
|
1129
|
+
All plumb steps have a `#pipeline` helper.
|
1070
1130
|
|
1071
|
-
|
1131
|
+
```ruby
|
1132
|
+
User = Types::Data[name: String, age: Integer]
|
1133
|
+
|
1134
|
+
CreateUser = User.pipeline do |pl|
|
1135
|
+
# Add steps as #call(Result) => Result interfaces
|
1136
|
+
pl.step ValidateUser.new
|
1137
|
+
|
1138
|
+
# Or as procs
|
1139
|
+
pl.step do |result|
|
1140
|
+
Logger.info "We have a valid user #{result.value}"
|
1141
|
+
result
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
# Or as other Plumb steps
|
1145
|
+
pl.step User.transform(User) { |user| user.with(name: user.name.upcase) }
|
1146
|
+
|
1147
|
+
pl.step do |result|
|
1148
|
+
DB.create(result.value)
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
# User normally as any other Plumb step
|
1153
|
+
result = CreateUser.resolve(name: 'Joe', age: 40)
|
1154
|
+
# result.valid?
|
1155
|
+
# result.errors
|
1156
|
+
# result.value => User
|
1157
|
+
```
|
1158
|
+
|
1159
|
+
Pipelines are Plumb steps, so they can be composed further.
|
1160
|
+
|
1161
|
+
```ruby
|
1162
|
+
IsJoe = User.check('must be named joe') { |user|
|
1163
|
+
result.value.name == 'Joe'
|
1164
|
+
}
|
1165
|
+
|
1166
|
+
CreateIfJoe = IsJoe >> CreateUser
|
1167
|
+
```
|
1168
|
+
|
1169
|
+
##### `#around`
|
1170
|
+
|
1171
|
+
Use `#around` in a pipeline definition to add a middleware step that wraps all other steps registered.
|
1172
|
+
|
1173
|
+
```ruby
|
1174
|
+
# The #around interface is #call(Step, Result::Valid) => Result::Valid | Result::Invalid
|
1175
|
+
StepLogger = proc do |step, result|
|
1176
|
+
Logger.info "Processing step #{step}"
|
1177
|
+
step.call(result)
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
CreateUser = User.pipeline do |pl|
|
1181
|
+
# Around middleware will wrap all other steps registered below
|
1182
|
+
pl.around StepLogger
|
1183
|
+
|
1184
|
+
pl.step ValidateUser.new
|
1185
|
+
pl.step ...etc
|
1186
|
+
end
|
1187
|
+
```
|
1188
|
+
|
1189
|
+
Note that order matters: an _around_ step will only wrap steps registered _after it_.
|
1190
|
+
|
1191
|
+
```ruby
|
1192
|
+
# This step will not be wrapper by StepLogger
|
1193
|
+
pl.step Step1
|
1194
|
+
|
1195
|
+
pl.around StepLogger
|
1196
|
+
# This step WILL be wrapped
|
1197
|
+
pl.step Step2
|
1198
|
+
```
|
1199
|
+
|
1200
|
+
Like regular steps, `around` middleware can be a class, an instance, a proc, or anything that implements the middleware interface.
|
1201
|
+
|
1202
|
+
```ruby
|
1203
|
+
# As class instance
|
1204
|
+
# pl.around StepLogger.new(:warn)
|
1205
|
+
class StepLogger
|
1206
|
+
def initialize(level = :info)
|
1207
|
+
@level = level
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
def call(step, result)
|
1211
|
+
Logger.send(@level) "Processing step #{step}"
|
1212
|
+
step.call(result)
|
1213
|
+
end
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
# As proc
|
1217
|
+
pl.around do |step, result|
|
1218
|
+
Logger.info "Processing step #{step}"
|
1219
|
+
step.call(result)
|
1220
|
+
end
|
1221
|
+
```
|
1222
|
+
|
1223
|
+
#### As stand-alone `Plumb::Pipeline` class
|
1224
|
+
|
1225
|
+
`Plumb::Pipeline` can also be used on its own, sub-classed, and it can take class-level `around` middleware.
|
1226
|
+
|
1227
|
+
```ruby
|
1228
|
+
class LoggedPipeline < Plumb::Pipeline
|
1229
|
+
# class-level midleware will be inherited by sub-classes
|
1230
|
+
around StepLogged
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
# Subclass inherits class-level middleware stack,
|
1234
|
+
# and it can also add its own class or instance-level middleware
|
1235
|
+
class ChildPipeline < LoggedPipeline
|
1236
|
+
# class-level middleware
|
1237
|
+
around Telemetry.new
|
1238
|
+
end
|
1239
|
+
|
1240
|
+
# Instantiate and add instance-level middleware
|
1241
|
+
pipe = ChildPipeline.new do |pl|
|
1242
|
+
pl.around NotifyErrors
|
1243
|
+
pl.step Step1
|
1244
|
+
pl.step Step2
|
1245
|
+
end
|
1246
|
+
```
|
1247
|
+
|
1248
|
+
Sub-classing `Plumb::Pipeline` can be useful to add helpers or domain-specific functionality
|
1249
|
+
|
1250
|
+
```ruby
|
1251
|
+
class DebuggablePipeline < LoggedPipeline
|
1252
|
+
# Use #debug! for inserting a debugger between steps
|
1253
|
+
def debug!
|
1254
|
+
step do |result|
|
1255
|
+
debugger
|
1256
|
+
result
|
1257
|
+
end
|
1258
|
+
end
|
1259
|
+
end
|
1260
|
+
|
1261
|
+
pipe = DebuggablePipeline.new do |pl|
|
1262
|
+
pl.step Step1
|
1263
|
+
pl.debug!
|
1264
|
+
pl.step Step2
|
1265
|
+
end
|
1266
|
+
```
|
1267
|
+
|
1268
|
+
#### Pipelines all the way down :turtle:
|
1269
|
+
|
1270
|
+
Pipelines are full Plumb steps, so they can themselves be used as steps.
|
1271
|
+
|
1272
|
+
```ruby
|
1273
|
+
Pipe1 = DebuggablePipeline.new do |pl|
|
1274
|
+
pl.step Step1
|
1275
|
+
pl.step Step2
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
Pipe2 = DebuggablePipeline.new do |pl|
|
1279
|
+
pl.step Pipe1 # <= A pipeline instance as step
|
1280
|
+
pl.step Step3
|
1281
|
+
end
|
1282
|
+
```
|
1283
|
+
|
1284
|
+
### Plumb::Schema
|
1072
1285
|
|
1073
1286
|
TODO
|
1074
1287
|
|
@@ -1388,7 +1601,7 @@ Types::DateTime.to_json_schema
|
|
1388
1601
|
- [ ] benchmarks and performace. Compare with `Parametric`, `ActiveModel::Attributes`, `ActionController::StrongParameters`
|
1389
1602
|
- [ ] flesh out `Plumb::Schema`
|
1390
1603
|
- [x] `Plumb::Struct`
|
1391
|
-
- [
|
1604
|
+
- [x] flesh out and document `Plumb::Pipeline`
|
1392
1605
|
- [ ] document custom visitors
|
1393
1606
|
- [ ] Improve errors, support I18n ?
|
1394
1607
|
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup(:benchmark)
|
5
|
+
|
6
|
+
require 'benchmark/ips'
|
7
|
+
require 'money'
|
8
|
+
Money.rounding_mode = BigDecimal::ROUND_HALF_EVEN
|
9
|
+
Money.default_currency = 'GBP'
|
10
|
+
require_relative './parametric_schema'
|
11
|
+
require_relative './plumb_hash'
|
12
|
+
|
13
|
+
data = {
|
14
|
+
supplier_name: 'Vodafone',
|
15
|
+
start_date: '2020-01-01',
|
16
|
+
end_date: '2021-01-11',
|
17
|
+
countdown_date: '2021-01-11',
|
18
|
+
name: 'Vodafone TV',
|
19
|
+
upfront_cost_description: 'Upfront cost description',
|
20
|
+
tv_channels_count: 100,
|
21
|
+
terms: [
|
22
|
+
{ name: 'Foo', url: 'http://foo.com', terms_text: 'Foo terms', start_date: '2020-01-01', end_date: '2021-01-01' },
|
23
|
+
{ name: 'Foo2', url: 'http://foo2.com', terms_text: 'Foo terms', start_date: '2020-01-01', end_date: '2021-01-01' }
|
24
|
+
],
|
25
|
+
tv_included: true,
|
26
|
+
additional_info: 'Additional info',
|
27
|
+
product_type: 'TV',
|
28
|
+
annual_price_increase_applies: true,
|
29
|
+
annual_price_increase_description: 'Annual price increase description',
|
30
|
+
broadband_components: [
|
31
|
+
{
|
32
|
+
name: 'Broadband 1',
|
33
|
+
technology: 'FTTP',
|
34
|
+
technology_tags: ['FTTP'],
|
35
|
+
is_mobile: false,
|
36
|
+
description: 'Broadband 1 description',
|
37
|
+
download_speed_measurement: 'Mbps',
|
38
|
+
download_speed: 100,
|
39
|
+
upload_speed_measurement: 'Mbps',
|
40
|
+
upload_speed: 100,
|
41
|
+
download_usage_limit: 1000,
|
42
|
+
discount_price: 100,
|
43
|
+
discount_period: 12,
|
44
|
+
speed_description: 'Speed description',
|
45
|
+
ongoing_price: 100,
|
46
|
+
contract_length: 12,
|
47
|
+
upfront_cost: 100,
|
48
|
+
commission: 100
|
49
|
+
}
|
50
|
+
],
|
51
|
+
tv_components: [
|
52
|
+
{
|
53
|
+
slug: 'vodafone-tv',
|
54
|
+
name: 'Vodafone TV',
|
55
|
+
search_tags: %w[Vodafone TV],
|
56
|
+
description: 'Vodafone TV description',
|
57
|
+
channels: 100,
|
58
|
+
discount_price: 100
|
59
|
+
}
|
60
|
+
],
|
61
|
+
call_package_types: ['Everything'],
|
62
|
+
phone_components: [
|
63
|
+
{
|
64
|
+
name: 'Phone 1',
|
65
|
+
description: 'Phone 1 description',
|
66
|
+
discount_price: 100,
|
67
|
+
disount_period: 12,
|
68
|
+
ongoing_price: 100,
|
69
|
+
contract_length: 12,
|
70
|
+
upfront_cost: 100,
|
71
|
+
commission: 100,
|
72
|
+
call_package_types: ['Everything']
|
73
|
+
}
|
74
|
+
],
|
75
|
+
payment_methods: ['Credit Card', 'Paypal'],
|
76
|
+
discounts: [
|
77
|
+
{ period: 12, price: 100 }
|
78
|
+
],
|
79
|
+
ongoing_price: 100,
|
80
|
+
contract_length: 12,
|
81
|
+
upfront_cost: 100,
|
82
|
+
year_1_price: 100,
|
83
|
+
savings: 100,
|
84
|
+
commission: 100,
|
85
|
+
max_broadband_download_speed: 100
|
86
|
+
}
|
87
|
+
|
88
|
+
# p V1Schemas::RECORD.resolve(data).errors
|
89
|
+
# p V2Schemas::Record.resolve(data)
|
90
|
+
# result = Parametric::V2::Result.wrap(data)
|
91
|
+
|
92
|
+
# p result
|
93
|
+
# p V2Schema.call(result)
|
94
|
+
Benchmark.ips do |x|
|
95
|
+
x.report('Parametric::Schema') do
|
96
|
+
ParametricSchema::RECORD.resolve(data)
|
97
|
+
end
|
98
|
+
x.report('Plumb') do
|
99
|
+
PlumbHash::Record.resolve(data)
|
100
|
+
end
|
101
|
+
x.compare!
|
102
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup(:benchmark)
|
5
|
+
|
6
|
+
require 'benchmark/ips'
|
7
|
+
require 'parametric/struct'
|
8
|
+
require 'plumb'
|
9
|
+
|
10
|
+
module ParametricStruct
|
11
|
+
class User
|
12
|
+
include Parametric::Struct
|
13
|
+
|
14
|
+
schema do
|
15
|
+
field(:name).type(:string).present
|
16
|
+
field(:friends).type(:array).schema do
|
17
|
+
field(:name).type(:string).present
|
18
|
+
field(:age).type(:integer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module PlumbStruct
|
25
|
+
include Plumb::Types
|
26
|
+
|
27
|
+
class User < Data
|
28
|
+
attribute :name, String.present
|
29
|
+
attribute :friends, Array do
|
30
|
+
attribute :name, String.present
|
31
|
+
attribute :age, Integer
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module DataBaseline
|
37
|
+
Friend = Data.define(:name, :age)
|
38
|
+
User = Data.define(:name, :friends) do
|
39
|
+
def self.build(data)
|
40
|
+
data = data.merge(friends: data[:friends].map { |friend| Friend.new(**friend) })
|
41
|
+
new(**data)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
data = {
|
47
|
+
name: 'John',
|
48
|
+
friends: [
|
49
|
+
{ name: 'Jane', age: 30 },
|
50
|
+
{ name: 'Joan', age: 38 }
|
51
|
+
]
|
52
|
+
}
|
53
|
+
|
54
|
+
Benchmark.ips do |x|
|
55
|
+
# x.report('Ruby Data') do
|
56
|
+
# user = DataBaseline::User.build(data)
|
57
|
+
# user.name
|
58
|
+
# end
|
59
|
+
x.report('Parametric::Struct') do
|
60
|
+
user = ParametricStruct::User.new(data)
|
61
|
+
user.name
|
62
|
+
end
|
63
|
+
x.report('Plumb::Types::Data') do
|
64
|
+
user = PlumbStruct::User.new(data)
|
65
|
+
user.name
|
66
|
+
end
|
67
|
+
x.compare!
|
68
|
+
end
|