runt 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +19 -0
  2. data/.travis.yml +5 -0
  3. data/{CHANGES → CHANGES.txt} +24 -8
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -44
  6. data/README.md +79 -0
  7. data/Rakefile +6 -119
  8. data/doc/tutorial_schedule.md +365 -0
  9. data/doc/tutorial_sugar.md +170 -0
  10. data/doc/tutorial_te.md +155 -0
  11. data/lib/runt.rb +36 -21
  12. data/lib/runt/dprecision.rb +4 -2
  13. data/lib/runt/pdate.rb +101 -95
  14. data/lib/runt/schedule.rb +18 -0
  15. data/lib/runt/sugar.rb +41 -9
  16. data/lib/runt/temporalexpression.rb +246 -30
  17. data/lib/runt/version.rb +3 -0
  18. data/runt.gemspec +24 -0
  19. data/site/.cvsignore +1 -0
  20. data/site/dcl-small.gif +0 -0
  21. data/site/index-rubforge-www.html +72 -0
  22. data/site/index.html +75 -60
  23. data/site/runt-logo.gif +0 -0
  24. data/site/runt-logo.psd +0 -0
  25. data/test/baseexpressiontest.rb +10 -8
  26. data/test/combinedexpressionstest.rb +166 -158
  27. data/test/daterangetest.rb +4 -6
  28. data/test/diweektest.rb +32 -32
  29. data/test/dprecisiontest.rb +2 -4
  30. data/test/everytetest.rb +6 -0
  31. data/test/expressionbuildertest.rb +2 -3
  32. data/test/icalendartest.rb +3 -6
  33. data/test/minitest_helper.rb +7 -0
  34. data/test/pdatetest.rb +21 -6
  35. data/test/redaytest.rb +3 -0
  36. data/test/reyeartest.rb +1 -1
  37. data/test/runttest.rb +5 -8
  38. data/test/scheduletest.rb +13 -14
  39. data/test/sugartest.rb +28 -6
  40. data/test/{spectest.rb → temporaldatetest.rb} +14 -4
  41. data/test/{rspectest.rb → temporalrangetest.rb} +4 -4
  42. data/test/test_runt.rb +11 -0
  43. data/test/weekintervaltest.rb +106 -0
  44. metadata +161 -116
  45. data/README +0 -106
  46. data/doc/tutorial_schedule.rdoc +0 -393
  47. data/doc/tutorial_sugar.rdoc +0 -143
  48. data/doc/tutorial_te.rdoc +0 -190
  49. data/setup.rb +0 -1331
@@ -1,393 +0,0 @@
1
- = Schedule Tutorial
2
-
3
- <em> This tutorial assumes you are familiar with use of the Runt API to
4
- create temporal expressions. If you're unfamiliar with how and why to write
5
- temporal expressions, take a look at the temporal expression
6
- tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_te_rdoc.html].</em>
7
-
8
- In his paper[http://martinfowler.com/apsupp/recurring.pdf] about recurring
9
- events, Martin Fowler also discusses a simple schedule API which is used,
10
- surprisingly enough, to build a schedule. We're not going to cover the pattern
11
- itself in this tutorial as Fowler already does a nice job. Because it is such
12
- a simple pattern (once you invent it!), you'll be able understand it even if
13
- you decide not to read his paper[http://martinfowler.com/apsupp/recurring.pdf].
14
-
15
- So, let's pretend that I own a car. Since I don't want to get a ticket, I
16
- decide to create an application which will tell me where and when I can park
17
- it on my street. (Since this is all make believe anyway, my car is a late 60's
18
- model black Ford Mustang with flame detailing (and on the back seat is one
19
- million dollars)).
20
-
21
- We'll build a Runt Schedule that models the parking regulations. Our app
22
- will check this Schedule at regular intervals and send us reminders to
23
- move our car so we don't get a ticket. YAY!
24
-
25
- First, let's visit the exciting world of NYC street cleaning regulations.
26
- Let's pretend the following rules are in place for our block:
27
-
28
- * For the north side of the street, there is no parking Monday, Wednesday, or Friday, from 8am thru 11am
29
-
30
- * For the south side of the street, there is no parking Tuesday or Thursday between 11:30am and 2pm
31
-
32
- Thus...
33
-
34
- ############################# #############################
35
- # # # #
36
- # NO PARKING # # NO PARKING #
37
- # # # #
38
- # Mon, Wed, Fri 8am-11am # # Tu, Th 11:30am-2:00pm #
39
- # # # #
40
- # # # #
41
- # Violators will be towed! # # Violaters will be towed! #
42
- # # # #
43
- ############################# #############################
44
- # # # #
45
- # # # #
46
- # # # #
47
-
48
- North side of the street South side of the street
49
-
50
-
51
- We'll start by creating temporal expressions which describe the verboten
52
- parking times:
53
-
54
-
55
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
56
-
57
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
58
-
59
-
60
- What we need at this point is a way to write queries against these expressions
61
- to determine whether we need to send a reminder. For this purpose, we can use
62
- a Schedule and an associated Event, both of which are supplied by Runt.
63
-
64
- schedule = Schedule.new
65
-
66
- A Schedule holds zero or more Event/TemporalExpression pairs, allowing clients
67
- to easily query and update TemporalExpressions as well perform certain range
68
- operations as we will see in a moment. We'll create two events, one for each
69
- side of the street:
70
-
71
- north_event = Event.new("north side")
72
-
73
- south_event = Event.new("south side")
74
-
75
- Now we add each event and its associated occurrence to our Schedule:
76
-
77
- schedule.add(north_event, north_expr)
78
-
79
- schedule.add(south_event, south_expr)
80
-
81
- An Event is simply a container for domain data. Although Runt uses Events
82
- by default, Schedules will happily house any kind of Object. Internally, a
83
- Schedule is really just a Hash where the keys are whatever it is you are
84
- scheduling and the values are the TemporalExpressions you create.
85
-
86
- class Schedule
87
- ...
88
-
89
- def add(obj, expression)
90
- @elems[obj]=expression
91
- end
92
- ...
93
-
94
- Now that we have a Schedule configured, we need something to check it and
95
- then let us know if we need to move the car. For this, we'll create a simple
96
- class called Reminder which will function as the "main-able" part of
97
- our app.
98
-
99
- We'll start by creating an easily testable constructor which will be passed
100
- a Schedule instance (like the one we just created) and an SMTP server.
101
-
102
-
103
- class Reminder
104
-
105
- attr_reader :schedule, :mail_server
106
-
107
- def initialize(schedule,mail_server)
108
- @schedule = schedule
109
- @mail_server = mail_server
110
- end
111
- ...
112
-
113
- Being devoted Agilists, we'll of course also create a unit test to
114
- help flesh out the specifics of our new Reminder class. We'll create
115
- test fixtures using the Runt Objects described above.
116
-
117
-
118
- class ReminderTest < Test::Unit::TestCase
119
-
120
- include Runt
121
-
122
- def setup
123
- @schedule = Schedule.new
124
- @north_event = Event.new("north side of the street will be ticketed")
125
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
126
- @schedule.add(@north_event, north_expr)
127
- @south_event = Event.new("south side of the street will be ticketed")
128
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
129
- @schedule.add(@south_event, south_expr)
130
- @mail_server = MailServer.new
131
- @reminder = Reminder.new(@schedule, @mail_server)
132
- @saturday_at_10 = PDate.min(2007,11,24,10,0,0)
133
- @monday_at_10 = PDate.min(2007,11,26,10,0,0)
134
- @tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
135
- end
136
-
137
- def test_initalize
138
- assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
139
- assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
140
- end
141
- ...
142
-
143
- For the purposes of this tutorial, the mail server will simply be a stub to
144
- illustrate how a real one might be used.
145
-
146
- class MailServer
147
-
148
- Struct.new("Email",:to,:from,:subject,:text)
149
-
150
- def send(to, from, subject, text)
151
- Struct::Email.new(to, from, subject, text)
152
- # etc...
153
- end
154
-
155
- end
156
-
157
- Next, let's add a method to our Reminder class which actually checks our
158
- schedule using a date which is passed in as a parameter.
159
-
160
- class Reminder
161
- ...
162
- def check(date)
163
- return @schedule.events(date)
164
- end
165
- ...
166
-
167
- The Schedule#events method will return an Array of Event Objects for any
168
- events which occur at the date and time given by the method's argument. Usage
169
- is easily demonstrated by a test case which makes use of the fixtures created
170
- by the TestCase#setup method defined above.
171
-
172
- class ReminderTest < Test::Unit::TestCase
173
- ...
174
- def test_check
175
- assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
176
- assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
177
- assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
178
- assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
179
- assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
180
- end
181
- ...
182
-
183
-
184
- There are other methods in the Schedule API which allow a client to query for
185
- information. Although we don't need them for this tutorial, I'll mention two
186
- briefly because they are generally useful. The first is Schedule#dates
187
- which will return an Array of PDate Objects which occur during the DateRange
188
- supplied as a parameter. The second is Schedule#include? which returns a
189
- boolean value indicating whether the Event occurs on the date which are both
190
- supplied as arguments.
191
-
192
- Next, let's make use of the mail server argument given to the Reminder class
193
- in it's constructor. This is the method that will be called when a call to the
194
- Reminder#check method produces results.
195
-
196
- class Reminder
197
- ...
198
- def send(date)
199
- text = "Warning: " + events.join(', ')
200
- return @mail_server.send(TO, FROM, SUBJECT, text)
201
- end
202
- ...
203
-
204
-
205
- Testing this is simple thanks to our MailServer stub which simply regurgitates
206
- the text argument it's passed as a result.
207
-
208
- class ReminderTest < Test::Unit::TestCase
209
- ...
210
- def test_send
211
- params = [@north_event, @south_event]
212
- result = @reminder.send(params)
213
- assert_email result, Reminder::TEXT + params.join(', ')
214
- end
215
-
216
- def assert_email(result, text)
217
- assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
218
- assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
219
- assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
220
- assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
221
- end
222
- ...
223
-
224
- Note the ReminderTest#assert_email method we've added to make assertions
225
- common to multiple test cases.
226
-
227
-
228
- Now, let's tie the whole thing together with a method which which checks for
229
- occuring Events and (upon finding some) sends a reminder. This method is
230
- really the only one in the Reminder class that needs to be public.
231
-
232
- class Reminder
233
- ...
234
- def run(date)
235
- result = self.check(date)
236
- self.send(result) if !result.empty?
237
- end
238
- ...
239
-
240
- class ReminderTest < Test::Unit::TestCase
241
- ...
242
- def test_send
243
- params = [@north_event, @south_event]
244
- result = @reminder.send(params)
245
- assert_email result, Reminder::TEXT + params.join(', ')
246
- end
247
- ...
248
-
249
- Finally, we'll cheat a bit and stitch every thing together so it can be run
250
- from a command line.
251
-
252
-
253
- include Runt
254
-
255
- schedule = Schedule.new
256
- north_event = Event.new("north side")
257
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
258
- schedule.add(north_event, north_expr)
259
- south_event = Event.new("south side")
260
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
261
- schedule.add(south_event, south_expr)
262
- reminder = Reminder.new(schedule, MailServer.new)
263
- while true
264
- sleep 15.minutes
265
- reminder.run Time.now
266
- end
267
-
268
- So, here's all the code for this tutorial (it's in the Runt distribution under
269
- the examples folder):
270
-
271
- ### schedule_tutorial.rb ###
272
-
273
- #!/usr/bin/ruby
274
-
275
- require 'runt'
276
-
277
- class Reminder
278
-
279
- TO = "me@myselfandi.com"
280
- FROM = "reminder@daemon.net"
281
- SUBJECT = "Move your car!"
282
- TEXT = "Warning: "
283
-
284
- attr_reader :schedule, :mail_server
285
-
286
- def initialize(schedule,mail_server)
287
- @schedule = schedule
288
- @mail_server = mail_server
289
- end
290
- def run(date)
291
- result = self.check(date)
292
- self.send(result) if !result.empty?
293
- end
294
- def check(date)
295
- puts "Checking the schedule..." if $DEBUG
296
- return @schedule.events(date)
297
- end
298
- def send(events)
299
- text = TEXT + events.join(', ')
300
- return @mail_server.send(TO, FROM, SUBJECT, text)
301
- end
302
- end
303
-
304
- class MailServer
305
- Struct.new("Email",:to,:from,:subject,:text)
306
- def send(to, from, subject, text)
307
- puts "Sending message TO: #{to} FROM: #{from} RE: #{subject}..." if $DEBUG
308
- Struct::Email.new(to, from, subject, text)
309
- # etc...
310
- end
311
- end
312
-
313
-
314
- if __FILE__ == $0
315
-
316
- include Runt
317
-
318
- schedule = Schedule.new
319
- north_event = Event.new("north side")
320
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
321
- schedule.add(north_event, north_expr)
322
- south_event = Event.new("south side")
323
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
324
- schedule.add(south_event, south_expr)
325
- reminder = Reminder.new(schedule, MailServer.new)
326
- while true
327
- sleep 15.minutes
328
- reminder.run Time.now
329
- end
330
-
331
- end
332
-
333
-
334
- ### schedule_tutorialtest.rb ###
335
-
336
- #!/usr/bin/ruby
337
-
338
- require 'test/unit'
339
- require 'runt'
340
- require 'schedule_tutorial'
341
-
342
- class ReminderTest < Test::Unit::TestCase
343
-
344
- include Runt
345
-
346
- def setup
347
- @schedule = Schedule.new
348
- @north_event = Event.new("north side of the street will be ticketed")
349
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
350
- @schedule.add(@north_event, north_expr)
351
- @south_event = Event.new("south side of the street will be ticketed")
352
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
353
- @schedule.add(@south_event, south_expr)
354
- @mail_server = MailServer.new
355
- @reminder = Reminder.new(@schedule, @mail_server)
356
- @saturday_at_10 = PDate.min(2007,11,24,10,0,0)
357
- @monday_at_10 = PDate.min(2007,11,26,10,0,0)
358
- @tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
359
- end
360
- def test_initalize
361
- assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
362
- assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
363
- end
364
- def test_send
365
- params = [@north_event, @south_event]
366
- result = @reminder.send(params)
367
- assert_email result, Reminder::TEXT + params.join(', ')
368
- end
369
- def test_check
370
- assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
371
- assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
372
- assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
373
- assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
374
- assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
375
- end
376
- def test_run
377
- result = @reminder.run(@monday_at_10)
378
- assert_email result, Reminder::TEXT + @north_event.to_s
379
- end
380
- def assert_email(result, text)
381
- assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
382
- assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
383
- assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
384
- assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
385
- end
386
- end
387
-
388
-
389
- <em>See Also:</em>
390
-
391
- * Fowler's recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf]
392
-
393
- * Other temporal patterns[http://martinfowler.com/ap2/timeNarrative.html]
@@ -1,143 +0,0 @@
1
- = Sugar Tutorial
2
-
3
- <em> This tutorial assumes you are familiar with use of the Runt API to
4
- create temporal expressions. If you're unfamiliar with how and why to write
5
- temporal expressions, take a look at the temporal expression
6
- tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_te_rdoc.html].</em>
7
-
8
- Starting with version 0.7.0, Runt provides some syntactic sugar for creating
9
- temporal expressions. Runt also provides a builder class for which can be
10
- used to create expressions in a more readable way than simply using :new.
11
-
12
- First, let's look at some of the new shorcuts for creating individual
13
- expressions. If you look at the lib/runt/sugar.rb file you find that the
14
- Runt module has been re-opened and some nutty stuff happens when
15
- :method_missing is called.
16
-
17
- For example, if you've included the Runt module, you can now create a
18
- DIWeek expression by calling a method whose name matches the following
19
- pattern:
20
-
21
- /^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/
22
-
23
- So
24
-
25
- tuesday
26
-
27
- is equivalent to
28
-
29
- DIWeek.new(Tuesday)
30
-
31
- Here's a quick summary of patterns and the expressions they create.
32
-
33
- === REDay
34
-
35
- <b>regex</b>:: /^daily_(\d{1,2})_(\d{2})([ap]m)_to_(\d{1,2})_(\d{2})([ap]m)$/
36
-
37
- <b>example</b>:: daily_8_30am_to_10_00pm
38
-
39
- <b>action</b>:: REDay.new(8,30,22,00)
40
-
41
- === REWeek
42
-
43
- <b>regex</b>:: /^weekly_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\_to\_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/
44
-
45
- <b>example</b>:: weekly_wednesday_to_friday
46
-
47
- <b>action</b>:: REWeek.new(Wednesday, Friday)
48
-
49
- === REMonth
50
-
51
- <b>regex</b>:: /^monthly_(\d{1,2})(?:st|nd|rd|th)\_to\_(\d{1,2})(?:st|nd|rd|th)$/
52
-
53
- <b>example</b>:: monthly_2nd_to_24th
54
-
55
- <b>action</b>:: REMonth.new(2,24)
56
-
57
- === REYear
58
-
59
- <b>regex</b>:: /^yearly_(january|february|march|april|may|june|july|august|september|october|november|december)_(\d{1,2})\_to\_(january|february|march|april|may|june|july|august|september|october|november|december)_(\d{1,2})
60
-
61
- <b>example</b>:: yearly_may_31_to_september_1
62
-
63
- <b>action</b>:: REYear.new(May,31,September,1)
64
-
65
- === DIWeek
66
-
67
- <b>regex</b>:: /^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/
68
-
69
- <b>example</b>:: friday
70
-
71
- <b>action</b>:: DIWeek.new(Friday)
72
-
73
- === DIMonth
74
-
75
- <b>regex</b>:: /^(first|second|third|fourth|last|second_to_last)_(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$/
76
-
77
- <b>example</b>:: last_friday
78
-
79
- <b>action</b>:: DIMonth.new(Last,Friday)
80
-
81
- Now let's look at the new ExpressionBuilder class. This class uses some simple methods and instance_eval to allow one to create composite tempooral expressions in a more fluid style than :new and friends. The idea is that you define a block where method calls add to a composite expression using either "and", "or", or "not".
82
-
83
- # Create a new builder
84
- b = ExpressionBuilder.new
85
-
86
- # Call define with a block
87
- expression = d.define do
88
- on REDay.new(8,45,9,30)
89
- on DIWeek.new(Friday) # "And"
90
- possibly DIWeek.new(Saturday) # "Or"
91
- except DIMonth.new(Last, Friday) # "Not"
92
- end
93
-
94
- # expression = "Daily 8:45am to 9:30 and Fridays or Saturday except not the last Friday of the month"
95
-
96
- Hmmm, this is not really an improvement over
97
-
98
-
99
- REDay.new(8,45,9,30) & DIWeek.new(Friday) | DIWeek.new(Saturday) - DIMonth.new(Last, Friday)
100
-
101
-
102
- I know, let's try the new constructor aliases defined above!
103
-
104
-
105
- expression = d.define do
106
- on daily_8_45am_to_9_30am
107
- on friday
108
- possibly saturday
109
- except last_friday
110
- end
111
-
112
- Much better, except "on daily..." seems a little awkward. We can use :occurs which is aliased to :on for just such a scenario.
113
-
114
-
115
- expression = d.define do
116
- occurs daily_8_45am_to_9_30am
117
- on friday
118
- possibly saturday
119
- except last_friday
120
- end
121
-
122
-
123
- ExpressionBuilder creates expressions by evaluating a block passed to the
124
- :define method. From inside the block, methods :occurs, :on, :every, :possibly,
125
- and :maybe can be called with a temporal expression which will be added to
126
- a composite expression as follows:
127
-
128
- * <b>:on</b>:: creates an "and" (&)
129
- * <b>:possibly</b>:: creates an "or" (|)
130
- * <b>:except</b>:: creates a "not" (-)
131
- * <b>:every</b>:: alias for :on method
132
- * <b>:occurs</b>:: alias for :on method
133
- * <b>:maybe</b>:: alias for :possibly method
134
-
135
- Of course it's easy to open the builder class and add you own aliases if the ones provided don't work for you:
136
-
137
- class ExpressionBuilder
138
- alias_method :potentially, :possibly
139
- etc....
140
- end
141
-
142
- If there are shortcuts or macros that you think others would find useful, send in a feature request or patch.
143
-