| Class | Result |
| In: |
app/models/result.rb
|
| Parent: | ActiveRecord::Base |
| updated_by | [RW] |
# File app/models/export/results.rb, line 5 5: def Result.export 6: Result.export_head 7: Result.export_data 8: end
# File app/models/export/results.rb, line 35
35: def Result.export_columns(fully_qualified = false)
36: if fully_qualified
37: columns = []
38: Result.export_columns_for_results.each do |column|
39: columns << "results." + column
40: end
41: Result.export_columns_for_people.each do |column|
42: columns << "people." + column
43: end
44: columns
45: else
46: Result.export_columns_for_results + Result.export_columns_for_people
47: end
48: end
# File app/models/export/results.rb, line 62
62: def Result.export_columns_for_people
63: [ "first_name", "last_name" ]
64: end
# File app/models/export/results.rb, line 50
50: def Result.export_columns_for_results
51: [
52: "id", "category_id", "person_id", "race_id", "team_id", "age",
53: "age_group", "date_of_birth", "gender", "license", "number", "city",
54: "state", "category_class", "place", "place_in_category", "points",
55: "points_from_place", "points_bonus", "points_penalty",
56: "points_bonus_penalty", "points_total", "time", "time_bonus_penalty",
57: "time_gap_to_leader", "time_gap_to_previous", "time_gap_to_winner",
58: "time_total", "laps", "is_series", "preliminary"
59: ]
60: end
# File app/models/export/results.rb, line 16
16: def Result.export_data
17: Base.export(Result.export_data_sql, "results.csv")
18: end
# File app/models/export/results.rb, line 26
26: def Result.export_data_sql
27: "SELECT #{Result.export_columns(true).join(",")}
28: INTO OUTFILE '%s'
29: FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'
30: LINES TERMINATED BY '\\n'
31: FROM results
32: LEFT JOIN people ON results.person_id=people.id"
33: end
# File app/models/export/results.rb, line 12
12: def Result.export_head
13: Base.export(Result.export_head_sql, "results.txt")
14: end
# File app/models/export/results.rb, line 20
20: def Result.export_head_sql
21: "SELECT '#{Result.export_columns.join("','")}'
22: INTO OUTFILE '%s'
23: FIELDS TERMINATED BY '\\n'"
24: end
# File app/models/result.rb, line 50
50: def Result.find_all_for(person)
51: if person.is_a? Person
52: person_id = person.id
53: else
54: person_id = person
55: end
56: Result.find(
57: :all,
58: :include => [:team, :person, :scores, :category, {:race => [:event, :category]}],
59: :conditions => ['people.id = ?', person_id]
60: )
61: end
All numbered places first, then blanks, followed by DNF, DQ, and DNS
# File app/models/result.rb, line 684
684: def <=>(other)
685: # Figure out the major position by place first, then break it down further if
686: begin
687: major_difference = (major_place <=> other.major_place)
688: return major_difference if major_difference != 0
689:
690: if place.to_i > 0
691: place.to_i <=> other.place.to_i
692: elsif self.id
693: self.id <=> other.id
694: else
695: 0
696: end
697: rescue ArgumentError => error
698: logger.error("Error in Result.<=> #{error} comparing #{self} with #{other}")
699: throw error
700: end
701: end
# File app/models/result.rb, line 201
201: def cache_attributes!(*args)
202: args = args.extract_options!
203: event.disable_notification!
204: cache_event_attributes if args.include?(:event)
205: cache_non_event_attributes if args.empty? || args.include?(:non_event)
206: save!
207: event.enable_notification!
208: end
# File app/models/result.rb, line 188
188: def cache_event_attributes
189: self[:competition_result] = competition_result?
190: self[:date] = event.date
191: self[:event_date_range_s] = event.date_range_s
192: self[:event_end_date] = event.end_date
193: self[:event_full_name] = event.full_name
194: self[:event_id] = event.id
195: self[:race_full_name] = race.try(:full_name)
196: self[:race_name] = race.try(:name)
197: self[:team_competition_result] = team_competition_result?
198: self.year = event.year
199: end
Cache expensive cross-table lookups
# File app/models/result.rb, line 180
180: def cache_non_event_attributes
181: self[:category_name] = category.try(:name)
182: self[:first_name] = person.try(:first_name, date)
183: self[:last_name] = person.try(:last_name, date)
184: self[:name] = person.try(:name, date)
185: self[:team_name] = team.try(:name, date)
186: end
# File app/models/result.rb, line 251
251: def category_name=(name)
252: if name.blank?
253: self.category = nil
254: else
255: self.category = Category.find_or_create_by_name(name)
256: end
257: self[:category_name] = name
258: end
Fix common formatting mistakes and inconsistencies
# File app/models/result.rb, line 536
536: def cleanup
537: cleanup_place
538: cleanup_number
539: cleanup_license
540: self.first_name = cleanup_name(first_name)
541: self.last_name = cleanup_name(last_name)
542: self.team_name = cleanup_name(team_name)
543: end
# File app/models/result.rb, line 569
569: def cleanup_license
570: self.license = license.to_s
571: # USAC license numbers are being imported with a one decimal zero, e.g., 12345.0
572: self.license = license.to_i.to_s if license[/^\d+\.0$/]
573: end
Mostly removes unfortunate punctuation typos
# File app/models/result.rb, line 576
576: def cleanup_name(name)
577: return name if name.nil?
578: name = name.to_s
579: return '' if name == '0.0'
580: return '' if name == '0'
581: return '' if name == '.'
582: return '' if name.include?('N/A')
583: name = name.gsub(';', '\'')
584: name = name.gsub(/ *\/ */, '/')
585: end
# File app/models/result.rb, line 564
564: def cleanup_number
565: self.number = number.to_s
566: self.number = number.to_i.to_s if number[/^\d+\.0$/]
567: end
Drops the ‘st’ from 1st, among other things
# File app/models/result.rb, line 546
546: def cleanup_place
547: if place
548: normalized_place = place.to_s.dup
549: normalized_place.upcase!
550: normalized_place.gsub!("ST", "")
551: normalized_place.gsub!("ND", "")
552: normalized_place.gsub!("RD", "")
553: normalized_place.gsub!("TH", "")
554: normalized_place.gsub!(")", "")
555: normalized_place = normalized_place.to_i.to_s if normalized_place[/^\d+\.0$/]
556: normalized_place.strip!
557: normalized_place.gsub!(".", "")
558: self.place = normalized_place
559: else
560: self.place = ""
561: end
562: end
# File app/models/result.rb, line 606
606: def compare_by_highest_place(other)
607: scores_by_place = scores.sort do |x, y|
608: x.source_result <=> y.source_result
609: end
610: other_scores_by_place = other.scores.sort do |x, y|
611: x.source_result <=> y.source_result
612: end
613: max_results = [ scores_by_place.size, other_scores_by_place.size ].max
614: return 0 if max_results == 0
615: for index in 0..(max_results - 1)
616: if scores_by_place.size == index
617: return 1
618: elsif other_scores_by_place.size == index
619: return -1
620: else
621: diff = scores_by_place[index].source_result.place <=> other_scores_by_place[index].source_result.place
622: return diff if diff != 0
623: end
624: end
625: 0
626: end
# File app/models/result.rb, line 628
628: def compare_by_most_recent_place(other)
629: dates = Set.new(scores + other.scores) { |score| score.source_result.date }.to_a
630: dates.sort!.reverse!
631: dates.each do |date|
632: score = scores.detect { |s| s.source_result.event.date == date }
633: other_score = other.scores.detect { |s| s.source_result.event.date == date }
634: if score && !other_score
635: return -1
636: elsif !score && other_score
637: return 1
638: else
639: diff = score.source_result.place <=> other_score.source_result.place
640: return diff if diff != 0
641: end
642: end
643: 0
644: end
Highest points first. Break ties by highest placing OBRA rules:
Fairly complicated and procedural, but in nearly all cases, it short-circuits after comparing points
# File app/models/result.rb, line 593
593: def compare_by_points(other, break_ties = true)
594: diff = other.points <=> points
595: return diff if diff != 0 || !break_ties
596:
597: diff = compare_by_highest_place(other)
598: return diff if diff != 0
599:
600: diff = compare_by_most_recent_place(other)
601: return diff if diff != 0
602:
603: 0
604: end
# File app/models/result.rb, line 260
260: def competition_result?
261: if competition_result.nil?
262: @competition_result = event.is_a?(Competition)
263: else
264: competition_result
265: end
266: end
# File app/models/result.rb, line 292
292: def date
293: if race || race(true)
294: race.date
295: end
296: end
Destroy People that only exist because they were created by importing results
# File app/models/result.rb, line 220
220: def destroy_people
221: if person && person.results.count == 0 && person.created_from_result? && !person.updated_after_created?
222: person.destroy
223: end
224: end
Destroy Team that only exist because they were created by importing results
# File app/models/result.rb, line 227
227: def destroy_teams
228: if team && team.results.count == 0 && team.people.count == 0 && team.created_from_result? && !team.updated_after_created?
229: team.destroy
230: end
231: end
# File app/models/result.rb, line 210
210: def ensure_custom_attributes
211: if race_id && race.custom_columns && !custom_attributes
212: self.custom_attributes = Hash.new
213: race.custom_columns.each do |key|
214: custom_attributes[key.to_sym] = nil
215: end
216: end
217: end
# File app/models/result.rb, line 306
306: def event(reload = false)
307: if race(reload)
308: race.event(reload)
309: end
310: end
# File app/models/result.rb, line 302
302: def event_id
303: self[:event_id] || (race || race(true)).try(:event_id)
304: end
Replace any new person, or team with one that already exists if name matches
# File app/models/result.rb, line 64
64: def find_associated_records
65: _person = person
66: if _person && _person.new_record?
67: if _person.name.blank?
68: self.person = nil
69: else
70: existing_people = find_people
71: if existing_people.size == 1
72: self.person = existing_people.to_a.first
73: elsif existing_people.size > 1
74: self.person = Person.select_by_recent_activity(existing_people)
75: end
76: end
77: end
78:
79: # This logic should be in Person
80: if person &&
81: RacingAssociation.current.add_members_from_results? &&
82: person.new_record? &&
83: person.first_name.present? &&
84: person.last_name.present? &&
85: person[:member_from].blank? &&
86: event.number_issuer.name == RacingAssociation.current.short_name &&
87: !RaceNumber.rental?(number, Discipline[event.discipline])
88:
89: person.member_from = race.date
90: end
91:
92: if team && team.new_record?
93: if team.name.blank?
94: self.team = nil
95: else
96: existing_team = Team.find_by_name_or_alias(team.name)
97: self.team = existing_team if existing_team
98: end
99: team.created_by = event if team && team.new_record?
100: end
101: true
102: end
Use first_name, last_name, race_number, team to figure out if person already exists. Returns an Array of People if there is more than one potential match
TODO refactor into methods or split responsibilities with Person? Need Event to match on race number. Event will not be set before result is saved to database
# File app/models/result.rb, line 109
109: def find_people(_event = event)
110: matches = Set.new
111:
112: #license first if present and source is reliable (USAC)
113: if RacingAssociation.current.eager_match_on_license? && license.present?
114: matches = matches + Person.find_all_by_license(license)
115: return matches if matches.size == 1
116: end
117:
118: # name
119: matches = matches + Person.find_all_by_name_or_alias(:first_name => first_name, :last_name => last_name)
120: return matches if matches.size == 1
121:
122: # number
123: if number.present?
124: if matches.size > 1
125: # use number to choose between same names
126: RaceNumber.find_all_by_value_and_event(number, _event).each do |race_number|
127: if matches.include?(race_number.person)
128: return [ race_number.person ]
129: end
130: end
131: elsif name.blank?
132: # no name, so try to match by number
133: matches = RaceNumber.find_all_by_value_and_event(number, _event).map(&:person)
134: end
135: end
136:
137: # team
138: unless team_name.blank?
139: team = Team.find_by_name_or_alias(team_name)
140: matches.reject! do |match|
141: match.team != team
142: end
143: return matches if matches.size == 1
144: end
145:
146: # license
147: unless self.license.blank?
148: matches.reject! do |match|
149: match.license != license
150: end
151: end
152:
153: matches
154: end
Not blank, DNF, DNS, DQ.
# File app/models/result.rb, line 277
277: def finished?
278: return false if place.blank?
279: return false if ["DNF", "DNS", "DQ"].include?(place)
280: place.to_i > 0
281: end
# File app/models/result.rb, line 344
344: def first_name=(value)
345: if self.person
346: self.person.first_name = value
347: else
348: self.person = Person.new(:first_name => value)
349: end
350: self[:first_name] = value
351: self[:name] = self.person.try(:name, date)
352: end
# File app/models/result.rb, line 312
312: def laps
313: self[:laps] || (race && race.laps)
314: end
Does this result belong to the last event in a MultiDayEvent?
# File app/models/result.rb, line 284
284: def last_event?
285: return false unless self.event && event.parent
286: return false unless event.parent.respond_to?(:parent)
287: return true unless event.parent
288:
289: !(event.parent && (event.parent.end_date != self.date))
290: end
# File app/models/result.rb, line 354
354: def last_name=(value)
355: if self.person
356: self.person.last_name = value
357: else
358: self.person = Person.new(:last_name => value)
359: end
360: self[:last_name] = value
361: self[:name] = self.person.try(:name, date)
362: end
Poor name. For comparison, we sort by placed, finished, DNF, etc
# File app/models/result.rb, line 647
647: def major_place
648: if place.to_i > 0
649: 0
650: elsif place.blank? || place == 0
651: 1
652: elsif place.upcase == 'DNF'
653: 2
654: elsif place.upcase == 'DQ'
655: 3
656: elsif place.upcase == 'DNS'
657: 4
658: else
659: 5
660: end
661: end
# File app/models/result.rb, line 667
667: def method_missing(sym, *args, &block)
668: # Performance fix for results page. :to_ary would be called and trigger a load of race to get custom_attrib
669: # May want to denormalize custom_attributes in Result.
670: if sym == :to_ary
671: return super
672: end
673:
674: if sym != :custom_attributes && custom_attributes && custom_attributes.has_key?(sym)
675: custom_attributes[sym]
676: elsif race && race.custom_columns && race.custom_columns.include?(sym)
677: nil
678: else
679: super
680: end
681: end
person.name
# File app/models/result.rb, line 369
369: def name=(value)
370: if value.present?
371: if person.try(:name) != value
372: self.person = Person.new(:name => value)
373: end
374: self[:first_name] = person.first_name
375: self[:last_name] = person.last_name
376: else
377: self.person = nil
378: self[:first_name] = nil
379: self[:last_name] = nil
380: end
381: self[:name] = value
382: end
# File app/models/result.rb, line 320
320: def points
321: if self[:points]
322: self[:points].to_f
323: else
324: 0.0
325: end
326: end
Hot spots
# File app/models/result.rb, line 329
329: def points_bonus_penalty=(value)
330: if value == nil || value == ""
331: value = 0
332: end
333: write_attribute(:points_bonus_penalty, value)
334: end
Points from placing at finish, not from hot spots
# File app/models/result.rb, line 337
337: def points_from_place=(value)
338: if value == nil || value == ""
339: value = 0
340: end
341: write_attribute(:points_from_place, value)
342: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75 This method doesn‘t handle some typical edge cases very well
# File app/models/result.rb, line 516
516: def s_to_time(string)
517: if string.to_s.blank?
518: 0.0
519: else
520: string.gsub!(',', '.')
521: parts = string.to_s.split(':')
522: parts.reverse!
523: t = 0.0
524: parts.each_with_index do |part, index|
525: t = t + (part.to_f) * (60.0 ** index)
526: end
527: if parts.last && parts.last.starts_with?("-")
528: -t
529: else
530: t
531: end
532: end
533: end
Save associated Person
# File app/models/result.rb, line 168
168: def save_person
169: logger.debug "save_person race.date: #{race.date} #{person.try :new_record?} #{person.try :changed?} member_from: #{person.try :member_from} member_to: #{person.try :member_to}"
170: if person && (person.new_record? || person.changed?)
171: if person.new_record?
172: person.created_by = event
173: end
174: logger.debug "SAVE!"
175: person.save!
176: end
177: end
# File app/models/result.rb, line 436
436: def set_time_value(attribute, value)
437: case value
438: when DateTime
439: self[attribute] = value.hour * 3600 + value.min * 60 + value.sec
440: when Time
441: self[attribute] = value.hour * 3600 + value.min * 60 + value.sec + (value.usec / 100.0)
442: when Numeric, NilClass
443: self[attribute] = value
444: else
445: self[attribute] = s_to_time(value)
446: end
447: end
# File app/models/result.rb, line 268
268: def team_competition_result?
269: if team_competition_result.nil?
270: @team_competition_result = event.is_a?(TeamBar) || event.is_a?(CrossCrusadeTeamCompetition) || event.is_a?(MbraTeamBar)
271: else
272: team_competition_result
273: end
274: end
# File app/models/result.rb, line 397
397: def team_name=(value)
398: team_id_will_change!
399: if team.nil? || team.name != value
400: self.team = Team.new(:name => value)
401: end
402: if person && person.team_name != value
403: person.team = team
404: end
405: self[:team_name] = value
406: end
# File app/models/result.rb, line 408
408: def time=(value)
409: set_time_value(:time, value)
410: end
# File app/models/result.rb, line 412
412: def time_bonus_penalty=(value)
413: set_time_value(:time_bonus_penalty, value)
414: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 470
470: def time_bonus_penalty_s
471: time_to_s(self.time_bonus_penalty)
472: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 475
475: def time_bonus_penalty_s=(time_bonus_penalty)
476: self.time_bonus_penalty = s_to_time(time_bonus_penalty)
477: end
# File app/models/result.rb, line 416
416: def time_gap_to_leader=(value)
417: set_time_value(:time_gap_to_leader, value)
418: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 480
480: def time_gap_to_leader_s
481: time_to_s(self.time_gap_to_leader)
482: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 485
485: def time_gap_to_leader_s=(time_gap_to_leader_s)
486: self.time_gap_to_leader = s_to_time(time_gap_to_leader_s)
487: end
# File app/models/result.rb, line 424
424: def time_gap_to_previous=(value)
425: set_time_value(:time_gap_to_previous, value)
426: end
# File app/models/result.rb, line 428
428: def time_gap_to_winner=(value)
429: set_time_value(:time_gap_to_winner, value)
430: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 490
490: def time_gap_to_winner_s
491: time_to_s time_gap_to_winner
492: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 450
450: def time_s
451: time_to_s(self.time)
452: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 455
455: def time_s=(time)
456: self.time = s_to_time(time)
457: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75 This method doesn‘t handle some typical edge cases very well
# File app/models/result.rb, line 496
496: def time_to_s(time)
497: return '' if time == 0.0 or time.blank?
498: positive = time >= 0
499:
500: if !positive
501: time = -time
502: end
503:
504: hours = (time / 3600).to_i
505: minutes = ((time - (hours * 3600)) / 60).floor
506: seconds = (time - (hours * 3600).floor - (minutes * 60).floor)
507: seconds = sprintf('%0.2f', seconds)
508: if hours > 0
509: hour_prefix = "#{hours.to_s.rjust(2, '0')}:"
510: end
511: "#{"-" unless positive}#{hour_prefix}#{minutes.to_s.rjust(2, '0')}:#{seconds.rjust(5, '0')}"
512: end
# File app/models/result.rb, line 432
432: def time_total=(value)
433: set_time_value(:time_total, value)
434: end
# File app/models/result.rb, line 420
420: def time_total=(value)
421: set_time_value(:time_total, value)
422: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 460
460: def time_total_s
461: time_to_s(self.time_total)
462: end
Time in hh:mm:ss.00 format. E.g., 1:20:59.75
# File app/models/result.rb, line 465
465: def time_total_s=(time_total)
466: self.time_total = s_to_time(time_total)
467: end
# File app/models/result.rb, line 708
708: def to_s
709: "#<Result #{id} place #{place} race #{race_id} person #{person_id} team #{team_id} pts #{points}>"
710: end
Set +person#number+ to number if this isn‘t a rental number FIXME optimize default number issuer business
# File app/models/result.rb, line 158
158: def update_person_number
159: discipline = Discipline[event.discipline]
160: default_number_issuer = NumberIssuer.find_by_name(RacingAssociation.current.short_name)
161: if person && event.number_issuer && event.number_issuer != default_number_issuer && number.present? && !RaceNumber.rental?(number, discipline)
162: person.updated_by = self.updated_by
163: person.add_number(number, discipline, event.number_issuer, event.date.year)
164: end
165: end