bookie_accounting 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,11 @@
1
1
  require 'bookie/config'
2
+ require 'bookie/extensions'
2
3
 
3
4
  require 'active_record'
4
5
 
5
6
  module Bookie
6
- #Contains ActiveRecord structures for the central database
7
- #
8
- #For a list of fields in the various models, see {Database Tables}[link:rdoc/database_rdoc.html]
7
+ ##
8
+ #Contains database-related code and models
9
9
  module Database
10
10
 
11
11
  ##
@@ -57,6 +57,8 @@ module Bookie
57
57
  def end_time
58
58
  return start_time + wall_time
59
59
  end
60
+
61
+ #To consider: disable #end_time= ?
60
62
 
61
63
  def self.by_user(user)
62
64
  where('jobs.user_id = ?', user.id)
@@ -83,7 +85,7 @@ module Bookie
83
85
  def self.by_group_name(group_name)
84
86
  group = Group.find_by_name(group_name)
85
87
  return joins(:user).where('users.group_id = ?', group.id) if group
86
- limit(0)
88
+ where('1=0')
87
89
  end
88
90
 
89
91
  ##
@@ -113,7 +115,13 @@ module Bookie
113
115
  ##
114
116
  #Finds all jobs whose running intervals overlap the given time range
115
117
  def self.by_time_range_inclusive(time_range)
116
- where('? <= jobs.end_time AND jobs.start_time < ?', time_range.first, time_range.last)
118
+ if time_range.empty?
119
+ where('1=0')
120
+ elsif time_range.exclude_end?
121
+ where('? <= jobs.end_time AND jobs.start_time < ?', time_range.first, time_range.last)
122
+ else
123
+ where('? <= jobs.end_time AND jobs.start_time <= ?', time_range.first, time_range.last)
124
+ end
117
125
  end
118
126
 
119
127
  ##
@@ -125,12 +133,14 @@ module Bookie
125
133
  #- <tt>:memory_time</tt>: the sum of memory * wall_time for all jobs in the interval
126
134
  #- <tt>:successful</tt>: the number of jobs that have completed successfully
127
135
  #
128
- #This method should probably not be used with other queries that filter by start/end time.
136
+ #This method should probably not be chained with other queries that filter by start/end time.
137
+ #
138
+ #To consider: filter out jobs with 0 CPU time?
129
139
  def self.summary(time_range = nil)
130
140
  time_range = time_range.normalized if time_range
131
141
  jobs = self
132
142
  jobs = jobs.by_time_range_inclusive(time_range) if time_range
133
- jobs = jobs.where('jobs.cpu_time > 0').all_with_relations
143
+ jobs = jobs.all_with_relations
134
144
  cpu_time = 0
135
145
  successful_jobs = 0
136
146
  memory_time = 0
@@ -151,10 +161,7 @@ module Bookie
151
161
  #To consider: what should I do about jobs that only report a max memory value?
152
162
  memory_time += job.memory * clipped_wall_time
153
163
  end
154
- #Only count the job as successful if it's actually finished by the end of the summary.
155
- if job.exit_code == 0 && (!time_range || job.end_time < time_range.end) then
156
- successful_jobs += 1
157
- end
164
+ successful_jobs += 1 if job.exit_code == 0
158
165
  end
159
166
 
160
167
  return {
@@ -171,40 +178,38 @@ module Bookie
171
178
  #Relations are not cached between calls.
172
179
  def self.all_with_relations
173
180
  jobs = all
174
- transaction do
175
- users = {}
176
- groups = {}
177
- systems = {}
178
- system_types = {}
179
- jobs.each do |job|
180
- system = systems[job.system_id]
181
- if system
182
- job.system = system
183
- else
184
- system = job.system
185
- systems[system.id] = system
186
- end
187
- system_type = system_types[system.system_type_id]
188
- if system_type
189
- system.system_type = system_type
190
- else
191
- system_type = system.system_type
192
- system_types[system_type.id] = system_type
193
- end
194
- user = users[job.user_id]
195
- if user
196
- job.user = user
197
- else
198
- user = job.user
199
- users[user.id] = user
200
- end
201
- group = groups[user.group_id]
202
- if group
203
- user.group = group
204
- else
205
- group = user.group
206
- groups[group.id] = group
207
- end
181
+ users = {}
182
+ groups = {}
183
+ systems = {}
184
+ system_types = {}
185
+ jobs.each do |job|
186
+ system = systems[job.system_id]
187
+ if system
188
+ job.system = system
189
+ else
190
+ system = job.system
191
+ systems[system.id] = system
192
+ end
193
+ system_type = system_types[system.system_type_id]
194
+ if system_type
195
+ system.system_type = system_type
196
+ else
197
+ system_type = system.system_type
198
+ system_types[system_type.id] = system_type
199
+ end
200
+ user = users[job.user_id]
201
+ if user
202
+ job.user = user
203
+ else
204
+ user = job.user
205
+ users[user.id] = user
206
+ end
207
+ group = groups[user.group_id]
208
+ if group
209
+ user.group = group
210
+ else
211
+ group = user.group
212
+ groups[group.id] = group
208
213
  end
209
214
  end
210
215
 
@@ -231,50 +236,89 @@ module Bookie
231
236
  end
232
237
  end
233
238
 
239
+ ##
240
+ #A cached summary of Jobs in the database
241
+ #
242
+ #Most summary operations should be performed through this class to improve efficiency.
234
243
  class JobSummary < ActiveRecord::Base
235
244
  self.table_name = :job_summaries
236
245
 
237
246
  belongs_to :user
238
247
  belongs_to :system
248
+
249
+ attr_accessible :date, :user, :user_id, :system, :system_id, :command_name, :cpu_time, :memory_time
239
250
 
251
+ ##
252
+ #Filters by date
240
253
  def self.by_date(date)
241
254
  where('job_summaries.date = ?', date)
242
255
  end
256
+
257
+ ##
258
+ #Filters by a date range
259
+ def self.by_date_range(range)
260
+ range = range.normalized
261
+ if range.exclude_end?
262
+ where('? <= job_summaries.date AND job_summaries.date < ?', range.begin, range.end)
263
+ else
264
+ where('? <= job_summaries.date AND job_summaries.date <= ?', range.begin, range.end)
265
+ end
266
+ end
243
267
 
268
+ ##
269
+ #Filters by user
244
270
  def self.by_user(user)
245
271
  where('job_summaries.user_id = ?', user.id)
246
272
  end
247
273
 
274
+ ##
275
+ #Filters by user name
248
276
  def self.by_user_name(name)
249
277
  joins(:user).where('users.name = ?', name)
250
278
  end
251
279
 
280
+ ##
281
+ #Filters by group
252
282
  def self.by_group(group)
253
283
  joins(:user).where('users.group_id = ?', group.id)
254
284
  end
255
285
 
286
+ ##
287
+ #Filters by group name
256
288
  def self.by_group_name(name)
257
289
  group = Group.find_by_name(name)
258
290
  return by_group(group) if group
259
- limit(0)
291
+ where('1=0')
260
292
  end
261
293
 
294
+ ##
295
+ #Filters by system
262
296
  def self.by_system(system)
263
297
  where('job_summaries.system_id = ?', system.id)
264
298
  end
265
299
 
300
+ ##
301
+ #Filters by system name
266
302
  def self.by_system_name(name)
267
303
  joins(:system).where('systems.name = ?', name)
268
304
  end
269
305
 
306
+ ##
307
+ #Filters by system type
270
308
  def self.by_system_type(type)
271
309
  joins(:system).where('systems.system_type_id = ?', type.id)
272
310
  end
273
311
 
312
+ ##
313
+ #Filters by command name
274
314
  def self.by_command_name(cmd)
275
315
  where('job_summaries.command_name = ?', cmd)
276
316
  end
277
317
 
318
+ ##
319
+ #Attempts to find a JobSummary with the given date, user_id, system_id, and command_name
320
+ #
321
+ #If one does not exist, a new JobSummary will be instantiated (but not saved to the database).
278
322
  def self.find_or_new(date, user_id, system_id, command_name)
279
323
  str = by_date(date).where(:user_id => user_id, :system_id => system_id).by_command_name(command_name).to_sql
280
324
  summary = by_date(date).where(:user_id => user_id, :system_id => system_id).by_command_name(command_name).first
@@ -287,27 +331,62 @@ module Bookie
287
331
  summary
288
332
  end
289
333
 
334
+ ##
335
+ #Create cached summaries for the given date
336
+ #
337
+ #The date is interpreted as being in UTC.
338
+ #
339
+ #If there is nothing to summarize, a dummy summary will be created.
340
+ #
341
+ #Uses Lock::synchronize internally; should not be used in transaction blocks
290
342
  def self.summarize(date)
291
343
  jobs = Job
292
344
  unscoped = self.unscoped
293
- time_range = date.to_time ... (date + 1).to_time
345
+ time_min = date.to_utc_time
346
+ time_range = time_min ... time_min + 1.days
294
347
  day_jobs = jobs.by_time_range_inclusive(time_range)
348
+
349
+ #Find the sets of unique values.
295
350
  value_sets = day_jobs.select('user_id, system_id, command_name').uniq
296
- value_sets.each do |set|
297
- summary_jobs = jobs.where(:user_id => set.user_id).where(:system_id => set.system_id).by_command_name(set.command_name)
298
- summary = summary_jobs.summary(time_range)
351
+ if value_sets.empty?
352
+ user = User.select(:id).first
353
+ system = System.select(:id).first
354
+ #If there are no users or no systems, we can't create the dummy summary, so just return.
355
+ return unless user && system
356
+ #Create a dummy summary so summary() doesn't keep trying to create one.
299
357
  Lock[:job_summaries].synchronize do
300
- sum = unscoped.find_or_new(date, set.user_id, set.system_id, set.command_name)
301
- #To consider: do we even need this field? (Time-range summaries don't use it because of overlap issues.)
302
- sum.num_jobs = summary[:jobs].length
303
- sum.cpu_time = summary[:cpu_time]
304
- sum.memory_time = summary[:memory_time]
305
- sum.successful = summary[:successful]
358
+ sum = unscoped.find_or_new(date, user.id, system.id, '')
359
+ sum.cpu_time = 0
360
+ sum.memory_time = 0
306
361
  sum.save!
307
362
  end
363
+ else
364
+ value_sets.each do |set|
365
+ summary_jobs = jobs.where(:user_id => set.user_id).where(:system_id => set.system_id).by_command_name(set.command_name)
366
+ summary = summary_jobs.summary(time_range)
367
+ Lock[:job_summaries].synchronize do
368
+ sum = unscoped.find_or_new(date, set.user_id, set.system_id, set.command_name)
369
+ sum.cpu_time = summary[:cpu_time]
370
+ sum.memory_time = summary[:memory_time]
371
+ sum.save!
372
+ end
373
+ end
308
374
  end
309
375
  end
310
376
 
377
+ ##
378
+ #Returns a summary of jobs in the database
379
+ #
380
+ #The following options are supported:
381
+ #- [<tt>:range</tt>] restricts the summary to a specific time interval (specified as a Range of Time objects)
382
+ #- [<tt>:jobs</tt>] the jobs on which the summary should operate
383
+ #
384
+ #Internally, this may call JobSummary::summary, which uses Lock#synchronize, so this should not be used inside a transaction block.
385
+ #
386
+ #When filtering, the same filters must be applied to both the Jobs and the JobSummaries. For example:
387
+ # jobs = Bookie::Database::Job.by_user_name('root')
388
+ # summaries = Bookie::Database::Job.by_user_name('root')
389
+ # puts summaries.summary(:jobs => jobs)
311
390
  def self.summary(opts = {})
312
391
  jobs = opts[:jobs] || Job
313
392
  range = opts[:range]
@@ -323,7 +402,7 @@ module Bookie
323
402
  first_started_system = System.order(:start_time).first
324
403
  range = first_started_system.start_time ... end_time
325
404
  else
326
- range = Date.new ... Date.new
405
+ range = Time.new ... Time.new
327
406
  end
328
407
  end
329
408
  range = range.normalized
@@ -333,54 +412,66 @@ module Bookie
333
412
  memory_time = 0
334
413
  successful = 0
335
414
 
336
- #Could there be partial days at the beginning/end?
337
- date_range = range
338
- unless range.begin.kind_of?(Date) && range.end.kind_of?(Date)
339
- date_begin = range.begin.to_date
340
- unless date_begin.to_time == range.begin
341
- date_begin += 1
342
- time_before_max = [date_begin.to_time, range.end].min
343
- time_before_min = range.begin
344
- summary = jobs.summary(time_before_min ... time_before_max)
415
+ #Is the beginning somewhere between days?
416
+ date_begin = range.begin.utc.to_date
417
+ unless date_begin.to_utc_time == range.begin
418
+ date_begin += 1
419
+ time_before_max = [date_begin.to_utc_time, range.end].min
420
+ time_before_min = range.begin
421
+ summary = jobs.summary(time_before_min ... time_before_max)
422
+ cpu_time += summary[:cpu_time]
423
+ memory_time += summary[:memory_time]
424
+ end
425
+
426
+ #Is the end somewhere between days?
427
+ date_end = range.end.utc.to_date
428
+ time_after_min = date_end.to_utc_time
429
+ unless time_after_min <= range.begin
430
+ time_after_max = range.end
431
+ time_after_range = Range.new(time_after_min, time_after_max, range.exclude_end?)
432
+ unless time_after_range.empty?
433
+ summary = jobs.summary(time_after_range)
345
434
  cpu_time += summary[:cpu_time]
346
435
  memory_time += summary[:memory_time]
347
- successful += summary[:successful]
348
436
  end
349
-
350
- date_end = range.end.to_date
351
- time_after_min = date_end.to_time
352
- unless time_after_min <= range.begin
353
- time_after_max = range.end
354
- time_after_range = Range.new(time_after_min, time_after_max, range.exclude_end?)
355
- unless time_after_range.empty?
356
- summary = jobs.summary(time_after_range)
357
- cpu_time += summary[:cpu_time]
358
- memory_time += summary[:memory_time]
359
- successful += summary[:successful]
360
- end
361
- end
362
-
363
- date_range = date_begin ... date_end
364
437
  end
365
438
 
439
+ date_range = date_begin ... date_end
440
+
366
441
  unscoped = self.unscoped
367
- date = date_range.begin
368
- while date_range.cover?(date) do
369
- #To do: what if there aren't any summaries to be made? Will we just run summarize() each time?
370
- summarize(date) if unscoped.by_date(date).empty?
371
- summaries = by_date(date)
372
- summaries.all.each do |summary|
373
- cpu_time += summary.cpu_time
374
- memory_time += summary.memory_time
375
- successful += summary.successful
376
- end
377
- date += 1
442
+ summaries = by_date_range(date_range).order(:date).all
443
+ index = 0
444
+ date_range.each do |date|
445
+ new_index = index
446
+ sum = summaries[new_index]
447
+ while sum && sum.date == date do
448
+ cpu_time += sum.cpu_time
449
+ memory_time += sum.memory_time
450
+ new_index += 1
451
+ sum = summaries[new_index]
452
+ end
453
+ #Did we actually process any summaries?
454
+ if new_index == index
455
+ #Nope. Create the summaries.
456
+ #To consider: optimize out the query?
457
+ unscoped.summarize(date)
458
+ sums = by_date(date)
459
+ sums.each do |sum|
460
+ cpu_time += sum.cpu_time
461
+ memory_time += sum.memory_time
462
+ end
463
+ end
378
464
  end
379
465
 
380
- time_range = Range.new(range.begin.to_time, range.end.to_time, range.exclude_end?)
381
- jobs = jobs.by_time_range_inclusive(time_range)
382
- num_jobs = (range && range.empty?) ? 0 : jobs.count
383
-
466
+ if range && range.empty?
467
+ num_jobs = 0
468
+ successful = 0
469
+ else
470
+ jobs = jobs.by_time_range_inclusive(range)
471
+ num_jobs = jobs.count
472
+ successful = jobs.where('jobs.exit_code = 0').count
473
+ end
474
+
384
475
  {
385
476
  :num_jobs => num_jobs,
386
477
  :cpu_time => cpu_time,
@@ -389,19 +480,19 @@ module Bookie
389
480
  }
390
481
  end
391
482
 
392
- validates_presence_of :user_id, :system_id, :date, :num_jobs, :cpu_time, :memory_time, :successful
483
+ validates_presence_of :user_id, :system_id, :date, :cpu_time, :memory_time
393
484
 
394
485
  validates_each :command_name do |record, attr, value|
395
486
  record.errors.add(attr, 'must not be nil') if value == nil
396
487
  end
397
488
 
398
- validates_each :num_jobs, :cpu_time, :memory_time, :successful do |record, attr, value|
489
+ validates_each :cpu_time, :memory_time do |record, attr, value|
399
490
  record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
400
491
  end
401
492
  end
402
493
 
403
494
  ##
404
- #A group
495
+ #A group of users
405
496
  class Group < ActiveRecord::Base
406
497
  has_many :users
407
498
 
@@ -488,13 +579,26 @@ module Bookie
488
579
  where('systems.system_type_id = ?', sys_type.id)
489
580
  end
490
581
 
582
+ ##
583
+ #Finds all systems whose running intervals overlap the given time range
584
+ #
585
+ #To do: unit test.
586
+ def self.by_time_range_inclusive(time_range)
587
+ if time_range.empty?
588
+ where('1=0')
589
+ elsif time_range.exclude_end?
590
+ where('(? <= systems.end_time OR systems.end_time IS NULL) AND systems.start_time < ?', time_range.first, time_range.last)
591
+ else
592
+ where('(? <= systems.end_time OR systems.end_time IS NULL) AND systems.start_time <= ?', time_range.first, time_range.last)
593
+ end
594
+ end
595
+
491
596
  ##
492
597
  #Finds the current system for a given sender and time
493
598
  #
494
599
  #This method also checks that this system's specifications are the same as those in the database and raises an error if they are different.
495
600
  #
496
601
  #This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
497
- #
498
602
  def self.find_current(sender, time = nil)
499
603
  time ||= Time.now
500
604
  config = sender.config
@@ -516,19 +620,42 @@ Please make sure that all previous systems with this hostname have been marked a
516
620
  system
517
621
  end
518
622
 
623
+ ##
624
+ #Returns an array of all systems, pre-loading relations to reduce the need for extra queries
625
+ #
626
+ #Relations are not cached between calls.
627
+ def self.all_with_relations
628
+ systems = all
629
+ system_types = {}
630
+ systems.each do |system|
631
+ system_type = system_types[system.system_type_id]
632
+ if system_type
633
+ system.system_type = system_type
634
+ else
635
+ system_type = system.system_type
636
+ system_types[system_type.id] = system_type
637
+ end
638
+ end
639
+ systems
640
+ end
641
+
519
642
  ##
520
643
  #Produces a summary of all the systems for the given time interval
521
644
  #
522
645
  #Returns a hash with the following fields:
523
- #- <tt>:avail_cpu_time</tt>: the total CPU time available for the interval
524
- #- <tt>:avail_memory_time</tt>: the total amount of memory-time available (in kilobyte-seconds)
525
- #- <tt>:avail_memory_avg</tt>: the average amount of memory available (in kilobytes)
646
+ #- [<tt>:systems</tt>] an array containing all systems that are active in the interval
647
+ #- [<tt>:avail_cpu_time</tt>] the total CPU time available for the interval
648
+ #- [<tt>:avail_memory_time</tt>] the total amount of memory-time available (in kilobyte-seconds)
649
+ #- [<tt>:avail_memory_avg</tt>] the average amount of memory available (in kilobytes)
526
650
  #
527
651
  #To consider: include the start/end times for the summary (especially if they aren't provided as arguments)?
528
652
  #
529
- #To do: make this and other summaries operate differently on inclusive and exclusive ranges.
530
- #(Current behavior is as if the range were always exclusive.)
653
+ #Notes:
654
+ #
655
+ #Results may be slightly off when an inclusive range is used.
656
+ #To consider: is this worth fixing?
531
657
  def self.summary(time_range = nil)
658
+ #To consider: how to handle time zones with Rails apps?
532
659
  current_time = Time.now
533
660
  #Sums that are actually returned
534
661
  avail_cpu_time = 0
@@ -537,14 +664,13 @@ Please make sure that all previous systems with this hostname have been marked a
537
664
  systems = System
538
665
  if time_range
539
666
  time_range = time_range.normalized
540
- #To consider: optimize as union of queries?
541
- systems = systems.where(
542
- 'systems.start_time < ? AND (systems.end_time IS NULL OR systems.end_time > ?)',
543
- time_range.last,
544
- time_range.first)
667
+ #To do: unit test.
668
+ systems = systems.by_time_range_inclusive(time_range)
545
669
  end
670
+
671
+ all_systems = systems.all_with_relations
546
672
 
547
- systems.all.each do |system|
673
+ all_systems.each do |system|
548
674
  system_start_time = system.start_time
549
675
  system_end_time = system.end_time
550
676
  #Is there a time range constraint?
@@ -579,6 +705,7 @@ Please make sure that all previous systems with this hostname have been marked a
579
705
  end
580
706
 
581
707
  {
708
+ :systems => all_systems,
582
709
  :avail_cpu_time => avail_cpu_time,
583
710
  :avail_memory_time => avail_memory_time,
584
711
  :avail_memory_avg => if wall_time_range == 0 then 0.0 else Float(avail_memory_time) / wall_time_range end,
@@ -721,7 +848,6 @@ Please make sure that all previous systems with this hostname have been marked a
721
848
  t.datetime :start_time, :null => false
722
849
  t.datetime :end_time
723
850
  t.integer :cores, :null => false
724
- #To consider: replace with a float? (more compact)
725
851
  t.integer :memory, :null => false, :limit => 8
726
852
  end
727
853
  change_table :systems do |t|
@@ -766,12 +892,14 @@ Please make sure that all previous systems with this hostname have been marked a
766
892
  t.integer :memory, :null => false
767
893
  t.integer :exit_code, :null => false
768
894
  end
895
+ #To do: more indices?
769
896
  change_table :jobs do |t|
770
897
  t.index :user_id
771
898
  t.index :system_id
772
899
  t.index :command_name
773
900
  t.index :start_time
774
901
  t.index :end_time
902
+ t.index :exit_code
775
903
  end
776
904
  end
777
905
 
@@ -787,13 +915,10 @@ Please make sure that all previous systems with this hostname have been marked a
787
915
  t.references :system, :null => false
788
916
  t.date :date, :null => false
789
917
  t.string :command_name, :null => false
790
- t.integer :num_jobs, :null => false
791
918
  t.integer :cpu_time, :null => false
792
919
  t.integer :memory_time, :null => false
793
- t.integer :successful, :null => false
794
920
  end
795
921
  change_table :job_summaries do |t|
796
- #To consider: reorder for optimum efficiency?
797
922
  t.index [:date, :user_id, :system_id, :command_name], :unique => true, :name => 'identity'
798
923
  t.index :command_name
799
924
  t.index :date
@@ -857,63 +982,4 @@ Please make sure that all previous systems with this hostname have been marked a
857
982
  end
858
983
  end
859
984
 
860
- ##
861
- #Reopened to add some useful methods
862
- class Range
863
- ##
864
- #If end < begin, returns an empty range (begin ... begin)
865
- #Otherwise, returns the original range
866
- def normalized
867
- return self.begin ... self.begin if self.end < self.begin
868
- self
869
- end
870
-
871
- ##
872
- #Returns the empty status of the range
873
- #
874
- #A range is empty if end < begin or if begin == end and exclude_end? is true.
875
- def empty?
876
- (self.end < self.begin) || (exclude_end? && (self.begin == self.end))
877
- end
878
985
 
879
- #This code probably works, but we're not using it anywhere.
880
- # def intersection(other)
881
- # self_n = self.normalized
882
- # other = other.normalized
883
- #
884
- # new_begin, new_end, exclude_end = nil
885
- #
886
- # if self_n.cover?(other.begin)
887
- # new_first = other.begin
888
- # elsif other.cover?(self_n.begin)
889
- # new_first = self_n.begin
890
- # end
891
- #
892
- # return self_n.begin ... self_n.begin unless new_first
893
- #
894
- # if self_n.cover?(other.end)
895
- # unless other.exclude_end? && other.end == self_n.begin
896
- # new_end = other.end
897
- # exclude_end = other.exclude_end?
898
- # end
899
- # elsif other.cover?(self_n.end)
900
- # unless self_n.exclude_end? && self_n.end == other.begin
901
- # new_end = self_n.end
902
- # exclude_end = self_n.exclude_end?
903
- # end
904
- # end
905
- #
906
- # #If we still haven't found new_end, try one more case:
907
- # unless new_end
908
- # if self_n.end == other.end
909
- # #We'll only get here if both ranges exclude their ends and have the same end.
910
- # new_end = self_n.end
911
- # exclude_end = true
912
- # end
913
- # end
914
- #
915
- # return self_n.begin ... self_n.begin unless new_end
916
- #
917
- # Range.new(new_begin, new_end, exclude_end)
918
- # end
919
- end