sportdb-readers 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,952 @@
1
+ # encoding: utf-8
2
+
3
+ module SportDb
4
+
5
+ class MatchReaderV2 ## todo/check: rename to MatchReaderV2 (use plural?) why? why not?
6
+
7
+ def self.read( path ) ## use - rename to read_file or from_file etc. - why? why not?
8
+ txt = File.open( path, 'r:utf-8' ).read
9
+ parse( txt )
10
+ end
11
+
12
+ def self.parse( txt )
13
+ recs = LeagueOutlineReader.parse( txt )
14
+ pp recs
15
+
16
+ recs.each do |rec|
17
+ league = Sync::League.find!( rec[:league] )
18
+ season = Sync::Season.find!( rec[:season] )
19
+
20
+ event = Sync::Event.find!( league: league, season: season )
21
+
22
+ ## todo/fix: set lang for now depending on league country!!!
23
+ parser = MatchParserSimpleV2.new( rec[:lines],
24
+ event.teams,
25
+ event.start_at )
26
+ round_recs, match_recs = parser.parse
27
+
28
+ round_recs.each do |round_rec|
29
+ round = Sync::Round.find_or_create( round_rec, event: event ) ## check: use/rename to EventRound why? why not?
30
+ end
31
+ match_recs.each do |match_rec|
32
+ match = Sync::Match.create_or_update( match_rec, event: event )
33
+ end
34
+ end
35
+
36
+ recs
37
+ end # method read
38
+ end # class MatchReaderV2
39
+ end # module SportDb
40
+
41
+
42
+
43
+ __END__
44
+
45
+ class GameReader
46
+
47
+ include LogUtils::Logging
48
+
49
+ ## make models available by default with namespace
50
+ # e.g. lets you use Usage instead of Model::Usage
51
+ include Models
52
+
53
+ attr_reader :event # returns event record
54
+
55
+ ## value helpers e.g. is_year?, is_taglist? etc.
56
+ include TextUtils::ValueHelper
57
+
58
+ include FixtureHelpers
59
+
60
+ def self.from_zip( zip_file, entry_path, more_attribs={} )
61
+
62
+ logger = LogKernel::Logger.root
63
+
64
+ ## get text content from zip
65
+ entry = zip_file.find_entry( entry_path )
66
+ event_text = entry.get_input_stream().read()
67
+ event_text = event_text.force_encoding( Encoding::UTF_8 )
68
+
69
+ ## hack:
70
+ ## support old event config format for now (will get removed later)
71
+ ## e.g. check for
72
+ ## teams:
73
+ ## 12 teams: etc.
74
+ if event_text =~ /^teams:/i ||
75
+ event_text =~ /^\d{1,2} teams:/i ||
76
+ event_text =~ /^start_at:/i
77
+ ## old format
78
+ puts "*** [DEPRECATED] old event config format w/ yaml, please use new plain text format >#{entry_path}<"
79
+ reader = EventReader.from_zip( zip_file, entry_path )
80
+ reader.read()
81
+ else
82
+ ## new format
83
+ reader = EventMetaReader.from_zip( zip_file, entry_path )
84
+ reader.read()
85
+ end
86
+
87
+ event = reader.event ## was fetch_event( name )
88
+ fixtures = reader.fixtures ## was fetch_event_fixtures( name )
89
+
90
+ ## add path to fixtures (use path from event e.g)
91
+ # - bl + at-austria!/2012_13/bl -> at-austria!/2012_13/bl
92
+ # - bl_ii + at-austria!/2012_13/bl -> at-austria!/2012_13/bl_ii
93
+
94
+ dir = File.dirname( entry_path ) # use dir for fixtures
95
+
96
+ fixtures_with_path = fixtures.map do |fx|
97
+ fx_new = "#{dir}/#{fx}.txt" # add path upfront
98
+ logger.debug "fx: #{fx_new} | >#{fx}< + >#{dir}<"
99
+ fx_new
100
+ end
101
+
102
+ ## fix-fix-fix: change file extension to ??
103
+ text_ary = []
104
+ fixtures_with_path.each do |fixture_path|
105
+ entry = zip_file.find_entry( fixture_path )
106
+
107
+ text = entry.get_input_stream().read()
108
+ text = text.force_encoding( Encoding::UTF_8 )
109
+
110
+ text_ary << text
111
+ end
112
+
113
+ self.from_string( event, text_ary, more_attribs )
114
+ end
115
+
116
+ def self.from_file( path, more_attribs={} )
117
+
118
+ logger = LogKernel::Logger.root
119
+
120
+ ### NOTE: fix-fix-fix - pass in event path!!!!!!! (not fixture path!!!!)
121
+
122
+ ## - ## note: assume/enfore utf-8 encoding (with or without BOM - byte order mark)
123
+ ## - ## - see textutils/utils.rb
124
+ ## - text = File.read_utf8( path )
125
+
126
+ event_text = File.read_utf8( path )
127
+
128
+ ## hack:
129
+ ## support old event config format for now (will get removed later)
130
+ ## e.g. check for
131
+ ## teams:
132
+ ## 12 teams: etc.
133
+ if event_text =~ /^teams:/i ||
134
+ event_text =~ /^\d{1,2} teams:/i ||
135
+ event_text =~ /^start_at:/i
136
+
137
+ ## old format
138
+ puts "*** [DEPRECATED] old event config format w/ yaml, please use new plain text format >#{path}<"
139
+ reader = EventReader.from_file( path )
140
+ reader.read()
141
+ else
142
+ ## new format
143
+ reader = EventMetaReader.from_file( path )
144
+ reader.read()
145
+ end
146
+
147
+
148
+ event = reader.event ## was fetch_event( name )
149
+ fixtures = reader.fixtures ## was fetch_event_fixtures( name )
150
+
151
+
152
+ ## add path to fixtures (use path from event e.g)
153
+ # - bl + at-austria!/2012_13/bl -> at-austria!/2012_13/bl
154
+ # - bl_ii + at-austria!/2012_13/bl -> at-austria!/2012_13/bl_ii
155
+
156
+ dir = File.dirname( path ) # use dir for fixtures
157
+
158
+ fixtures_with_path = fixtures.map do |fx|
159
+ fx_new = "#{dir}/#{fx}.txt" # add path upfront
160
+ logger.debug "fx: #{fx_new} | >#{fx}< + >#{dir}<"
161
+ fx_new
162
+ end
163
+
164
+ ## fix-fix-fix: change file extension to ??
165
+ text_ary = []
166
+ fixtures_with_path.each do |fixture_path|
167
+ text_ary << File.read_utf8( fixture_path )
168
+ end
169
+
170
+ self.from_string( event, text_ary, more_attribs )
171
+ end
172
+
173
+
174
+ def self.from_string( event, text_or_text_ary, more_attribs={} )
175
+ ### fix - fix -fix:
176
+ ## change event to event_or_event_key !!!!! - allow event_key as string passed in
177
+ self.new( event, text_or_text_ary, more_attribs )
178
+ end
179
+
180
+
181
+ def initialize( event, text_or_text_ary, more_attribs={} )
182
+ ### fix - fix -fix:
183
+ ## change event to event_or_event_key !!!!! - allow event_key as string passed in
184
+
185
+ ## todo/fix: how to add opts={} ???
186
+ @event = event
187
+ @text_or_text_ary = text_or_text_ary
188
+ @more_attribs = more_attribs
189
+ end
190
+
191
+
192
+ def read
193
+ if @text_or_text_ary.is_a?( String )
194
+ text_ary = [@text_or_text_ary]
195
+ else
196
+ text_ary = @text_or_text_ary
197
+ end
198
+
199
+ ## reset cached values
200
+ ## for auto-number rounds etc.
201
+ @last_round_pos = nil
202
+
203
+ text_ary.each do |text|
204
+ ## assume en for now? why? why not?
205
+ ## fix (cache) store lang in event table (e.g. auto-add and auto-update)!!!
206
+ SportDb.lang.lang = SportDb.lang.classify( text )
207
+
208
+ reader = LineReader.from_string( text )
209
+
210
+ read_fixtures_worker( @event.key, reader )
211
+ end
212
+
213
+ ## fix add prop ??
214
+ ### Prop.create!( key: "db.#{fixture_name_to_prop_key(name)}.version", value: "file.txt.#{File.mtime(path).strftime('%Y.%m.%d')}" )
215
+ end
216
+
217
+
218
+ def read_fixtures_worker( event_key, reader )
219
+ ## NB: assume active activerecord connection
220
+
221
+ ## reset cached values
222
+ @patch_round_ids_dates = []
223
+ @patch_round_ids_pos = []
224
+
225
+ @round = nil ## fix: change/rename to @last_round !!!
226
+ @group = nil ## fix: change/rename to @last_group !!!
227
+ @last_date = nil
228
+
229
+ @last_team1 = nil # used for goals (to match players via squads)
230
+ @last_team2 = nil
231
+ @last_game = nil
232
+
233
+
234
+ #####
235
+ # fix: move to read and share event/known_teams
236
+ # for all 1-n fixture files (no need to configure every time!!)
237
+
238
+ @event = Event.find_by_key!( event_key )
239
+
240
+ logger.debug "Event #{@event.key} >#{@event.title}<"
241
+
242
+ ### fix: use build_title_table_for ??? why? why not??
243
+ ## @known_teams = @event.known_teams_table
244
+
245
+ @mapper_teams = TeamMapper.new( @event.teams )
246
+
247
+
248
+ @known_grounds = TextUtils.build_title_table_for( @event.grounds )
249
+
250
+
251
+ parse_fixtures( reader )
252
+
253
+ end # method load_fixtures
254
+
255
+
256
+
257
+ def parse_group_header( line )
258
+ logger.debug "parsing group header line: >#{line}<"
259
+
260
+ # note: group header resets (last) round (allows, for example):
261
+ # e.g.
262
+ # Group Playoffs/Replays -- round header
263
+ # team1 team2 -- match
264
+ # Group B: -- group header
265
+ # team1 team2 - match (will get new auto-matchday! not last round)
266
+ @round = nil ## fix: change/rename to @last_round !!!
267
+
268
+ title, pos = find_group_title_and_pos!( line )
269
+
270
+ logger.debug " title: >#{title}<"
271
+ logger.debug " pos: >#{pos}<"
272
+ logger.debug " line: >#{line}<"
273
+
274
+ # set group for games
275
+ @group = Group.find_by_event_id_and_pos!( @event.id, pos )
276
+ end
277
+
278
+
279
+ def parse_group_def( line )
280
+ logger.debug "parsing group def line: >#{line}<"
281
+
282
+ @mapper_teams.map_teams!( line )
283
+ team_keys = @mapper_teams.find_teams!( line )
284
+
285
+ title, pos = find_group_title_and_pos!( line )
286
+
287
+ logger.debug " line: >#{line}<"
288
+
289
+ group_attribs = {
290
+ title: title
291
+ }
292
+
293
+ group = Group.find_by_event_id_and_pos( @event.id, pos )
294
+ if group.present?
295
+ logger.debug "update group #{group.id}:"
296
+ else
297
+ logger.debug "create group:"
298
+ group = Group.new
299
+ group_attribs = group_attribs.merge( {
300
+ event_id: @event.id,
301
+ pos: pos
302
+ })
303
+ end
304
+
305
+ logger.debug group_attribs.to_json
306
+
307
+ group.update_attributes!( group_attribs )
308
+
309
+ group.teams.clear # remove old teams
310
+ ## add new teams
311
+ team_keys.each do |team_key|
312
+ team = Team.find_by_key!( team_key )
313
+ logger.debug " adding team #{team.title} (#{team.code})"
314
+ group.teams << team
315
+ end
316
+ end
317
+
318
+
319
+ def parse_round_def( line )
320
+ logger.debug "parsing round def line: >#{line}<"
321
+
322
+ ### todo/fix/check: move cut off optional comment in reader for all lines? why? why not?
323
+ cut_off_end_of_line_comment!( line ) # cut off optional comment starting w/ #
324
+
325
+ start_at = find_date!( line, start_at: @event.start_at )
326
+ end_at = find_date!( line, start_at: @event.start_at )
327
+
328
+ # note: if end_at missing -- assume start_at is (==) end_at
329
+ end_at = start_at if end_at.nil?
330
+
331
+ # note: - NOT needed; start_at and end_at are saved as date only (NOT datetime)
332
+ # set hours,minutes,secs to beginning and end of day (do NOT use default 12.00)
333
+ # e.g. use 00.00 and 23.59
334
+ # start_at = start_at.beginning_of_day
335
+ # end_at = end_at.end_of_day
336
+
337
+ # note: make sure start_at/end_at is date only (e.g. use start_at.to_date)
338
+ # sqlite3 saves datetime in date field as datetime, for example (will break date compares later!)
339
+ start_at = start_at.to_date
340
+ end_at = end_at.to_date
341
+
342
+
343
+ pos = find_round_pos!( line )
344
+ title = find_round_def_title!( line )
345
+ # NB: use extracted round title for knockout check
346
+ knockout_flag = is_knockout_round?( title )
347
+
348
+
349
+ logger.debug " start_at: #{start_at}"
350
+ logger.debug " end_at: #{end_at}"
351
+ logger.debug " pos: #{pos}"
352
+ logger.debug " title: >#{title}<"
353
+ logger.debug " knockout_flag: #{knockout_flag}"
354
+
355
+ logger.debug " line: >#{line}<"
356
+
357
+ #######################################
358
+ # fix: add auto flag is false !!!!
359
+
360
+ round_attribs = {
361
+ title: title,
362
+ knockout: knockout_flag,
363
+ start_at: start_at,
364
+ end_at: end_at
365
+ }
366
+
367
+ round = Round.find_by_event_id_and_pos( @event.id, pos )
368
+ if round.present?
369
+ logger.debug "update round #{round.id}:"
370
+ else
371
+ logger.debug "create round:"
372
+ round = Round.new
373
+
374
+ round_attribs = round_attribs.merge( {
375
+ event_id: @event.id,
376
+ pos: pos
377
+ })
378
+ end
379
+
380
+ logger.debug round_attribs.to_json
381
+
382
+ round.update_attributes!( round_attribs )
383
+ end
384
+
385
+
386
+ def parse_round_header( line )
387
+ logger.debug "parsing round header line: >#{line}<"
388
+
389
+ ### todo/fix/check: move cut off optional comment in reader for all lines? why? why not?
390
+ cut_off_end_of_line_comment!( line ) # cut off optional comment starting w/ #
391
+
392
+ # NB: cut off optional title2 starting w/ // first
393
+ title2 = find_round_header_title2!( line )
394
+
395
+ # todo/fix: check if it is possible title2 w/ group?
396
+ # add an example here
397
+ group_title, group_pos = find_group_title_and_pos!( line )
398
+
399
+ ## todo/check/fix:
400
+ # make sure Round of 16 will not return pos 16 -- how? possible?
401
+ # add unit test too to verify
402
+ pos = find_round_pos!( line )
403
+
404
+ ## check if pos available; if not auto-number/calculate
405
+ if pos.nil?
406
+ if @patch_round_ids_pos.empty?
407
+ pos = (@last_round_pos||0)+1
408
+ logger.debug( " no round pos found; auto-number round - use (#{pos})" )
409
+ else
410
+ # note: if any rounds w/o pos already seen (add for auto-numbering at the end)
411
+ # will get auto-numbered sorted by start_at date
412
+ pos = 999001+@patch_round_ids_pos.length # e.g. 999<count> - 999001,999002,etc.
413
+ logger.debug( " no round pos found; auto-number round w/ patch (backtrack) at the end" )
414
+ end
415
+ end
416
+
417
+ # store pos for auto-number next round if missing
418
+ # - note: only if greater/bigger than last; use max
419
+ # - note: last_round_pos might be nil - thus set to 0
420
+ if pos > 999000
421
+ # note: do NOT update last_round_pos for to-be-patched rounds
422
+ else
423
+ @last_round_pos = [pos,@last_round_pos||0].max
424
+ end
425
+
426
+
427
+ title = find_round_header_title!( line )
428
+
429
+ ## NB: use extracted round title for knockout check
430
+ knockout_flag = is_knockout_round?( title )
431
+
432
+
433
+ if group_pos.present?
434
+ @group = Group.find_by_event_id_and_pos!( @event.id, group_pos )
435
+ else
436
+ @group = nil # reset group to no group
437
+ end
438
+
439
+ logger.debug " line: >#{line}<"
440
+
441
+ ## NB: dummy/placeholder start_at, end_at date
442
+ ## replace/patch after adding all games for round
443
+
444
+ round_attribs = {
445
+ title: title,
446
+ title2: title2,
447
+ knockout: knockout_flag
448
+ }
449
+
450
+ if pos > 999000
451
+ # no pos (e.g. will get autonumbered later) - try match by title for now
452
+ # e.g. lets us use title 'Group Replays', for example, multiple times
453
+ @round = Round.find_by_event_id_and_title( @event.id, title )
454
+ else
455
+ @round = Round.find_by_event_id_and_pos( @event.id, pos )
456
+ end
457
+
458
+ if @round.present?
459
+ logger.debug "update round #{@round.id}:"
460
+ else
461
+ logger.debug "create round:"
462
+ @round = Round.new
463
+
464
+ round_attribs = round_attribs.merge( {
465
+ event_id: @event.id,
466
+ pos: pos,
467
+ start_at: Date.parse('1911-11-11'),
468
+ end_at: Date.parse('1911-11-11')
469
+ })
470
+ end
471
+
472
+ logger.debug round_attribs.to_json
473
+
474
+ @round.update_attributes!( round_attribs )
475
+
476
+ @patch_round_ids_pos << @round.id if pos > 999000
477
+ ### store list of round ids for patching start_at/end_at at the end
478
+ @patch_round_ids_dates << @round.id # todo/fix/check: check if round has definition (do NOT patch if definition (not auto-added) present)
479
+ end
480
+
481
+
482
+ def try_parse_game( line )
483
+ # note: clone line; for possible test do NOT modify in place for now
484
+ # note: returns true if parsed, false if no match
485
+ parse_game( line.dup )
486
+ end
487
+
488
+ def parse_game( line )
489
+ logger.debug "parsing game (fixture) line: >#{line}<"
490
+
491
+ @mapper_teams.map_teams!( line ) ### todo/fix: limit mapping to two(2) teams - why? why not? might avoid matching @ Barcelona ??
492
+ team_keys = @mapper_teams.find_teams!( line )
493
+ team1_key = team_keys[0]
494
+ team2_key = team_keys[1]
495
+
496
+ ## note: if we do NOT find two teams; return false - no match found
497
+ if team1_key.nil? || team2_key.nil?
498
+ logger.debug " no game match (two teams required) found for line: >#{line}<"
499
+ return false
500
+ end
501
+
502
+ pos = find_game_pos!( line )
503
+
504
+ if is_postponed?( line )
505
+ postponed = true
506
+ date_v2 = find_date!( line, start_at: @event.start_at )
507
+ date = find_date!( line, start_at: @event.start_at )
508
+ else
509
+ postponed = false
510
+ date_v2 = nil
511
+ date = find_date!( line, start_at: @event.start_at )
512
+ end
513
+
514
+ ###
515
+ # check if date found?
516
+ # NB: ruby falsey is nil & false only (not 0 or empty array etc.)
517
+ if date
518
+ ### check: use date_v2 if present? why? why not?
519
+ @last_date = date # keep a reference for later use
520
+ else
521
+ date = @last_date # no date found; (re)use last seen date
522
+ end
523
+
524
+
525
+ scores = find_scores!( line )
526
+
527
+
528
+ ####
529
+ # note:
530
+ # only map ground if we got any grounds (setup/configured in event)
531
+
532
+ if @event.grounds.count > 0
533
+
534
+ ## todo/check: use @known_grounds for check?? why? why not??
535
+ ## use in @known_grounds = TextUtils.build_title_table_for( @event.grounds )
536
+
537
+ ##
538
+ # fix: mark mapped title w/ type (ground-) or such!! - too avoid fallthrough match
539
+ # e.g. three teams match - but only two get mapped, third team gets match for ground
540
+ # e.g Somalia v Djibouti @ Djibouti
541
+ map_ground!( line )
542
+ ground_key = find_ground!( line )
543
+ ground = ground_key.nil? ? nil : Ground.find_by_key!( ground_key )
544
+ else
545
+ # no grounds configured; always nil
546
+ ground = nil
547
+ end
548
+
549
+ logger.debug " line: >#{line}<"
550
+
551
+
552
+ ### todo: cache team lookups in hash?
553
+
554
+ team1 = Team.find_by_key!( team1_key )
555
+ team2 = Team.find_by_key!( team2_key )
556
+
557
+ @last_team1 = team1 # store for later use for goals etc.
558
+ @last_team2 = team2
559
+
560
+
561
+ if @round.nil?
562
+ ## no round header found; calculate round from date
563
+
564
+ ###
565
+ ## todo/fix: add some unit tests for round look up
566
+ # fix: use date_v2 if present!! (old/original date; otherwise use date)
567
+
568
+ #
569
+ # fix: check - what to do with hours e.g. start_at use 00:00 and for end_at use 23.59 ??
570
+ # -- for now - remove hours (e.g. use end_of_day and beginnig_of_day)
571
+
572
+ ##
573
+ # note: start_at and end_at are dates ONLY (note datetime)
574
+ # - do NOT pass in hours etc. in query
575
+ # again use --> date.end_of_day, date.beginning_of_day
576
+ # new: not working: date.to_date, date.to_date
577
+ # will not find round if start_at same as date !! (in theory hours do not matter)
578
+
579
+ ###
580
+ # hack:
581
+ # special case for sqlite3 (date compare not working reliable; use casts)
582
+ # fix: move to adapter_name to activerecord_utils as sqlite? or similar?
583
+
584
+ if ActiveRecord::Base.connection.adapter_name.downcase.starts_with?( 'sqlite' )
585
+ logger.debug( " [sqlite] using sqlite-specific query for date compare for rounds finder" )
586
+ round = Round.where( 'event_id = ? AND ( julianday(start_at) <= julianday(?)'+
587
+ 'AND julianday(end_at) >= julianday(?))',
588
+ @event.id, date.to_date, date.to_date).first
589
+ else # all other dbs (postgresql, mysql, etc.)
590
+ round = Round.where( 'event_id = ? AND (start_at <= ? AND end_at >= ?)',
591
+ @event.id, date.to_date, date.to_date).first
592
+ end
593
+
594
+ pp round
595
+ if round.nil?
596
+ logger.warn( " !!!! no round match found for date #{date}" )
597
+ pp Round.all
598
+
599
+ ###################################
600
+ # -- try auto-adding matchday
601
+ round = Round.new
602
+
603
+ round_attribs = {
604
+ event_id: @event.id,
605
+ title: "Matchday #{date.to_date}",
606
+ pos: 999001+@patch_round_ids_pos.length, # e.g. 999<count> - 999001,999002,etc.
607
+ start_at: date.to_date,
608
+ end_at: date.to_date
609
+ }
610
+
611
+ logger.info( " auto-add round >Matchday #{date.to_date}<" )
612
+ logger.debug round_attribs.to_json
613
+
614
+ round.update_attributes!( round_attribs )
615
+
616
+ @patch_round_ids_pos << round.id # todo/check - add just id or "full" record as now - why? why not?
617
+ end
618
+
619
+ # store pos for auto-number next round if missing
620
+ # - note: only if greater/bigger than last; use max
621
+ # - note: last_round_pos might be nil - thus set to 0
622
+ if round.pos > 999000
623
+ # note: do NOT update last_round_pos for to-be-patched rounds
624
+ else
625
+ @last_round_pos = [round.pos,@last_round_pos||0].max
626
+ end
627
+
628
+ ## note: will crash (round.pos) if round is nil
629
+ logger.debug( " using round #{round.pos} >#{round.title}< start_at: #{round.start_at}, end_at: #{round.end_at}" )
630
+ else
631
+ ## use round from last round header
632
+ round = @round
633
+ end
634
+
635
+
636
+ ### check if games exists
637
+ ## with this teams in this round if yes only update
638
+ game = Game.find_by_round_id_and_team1_id_and_team2_id(
639
+ round.id, team1.id, team2.id
640
+ )
641
+
642
+ game_attribs = {
643
+ score1i: scores[0],
644
+ score2i: scores[1],
645
+ score1: scores[2],
646
+ score2: scores[3],
647
+ score1et: scores[4],
648
+ score2et: scores[5],
649
+ score1p: scores[6],
650
+ score2p: scores[7],
651
+ play_at: date,
652
+ play_at_v2: date_v2,
653
+ postponed: postponed,
654
+ knockout: round.knockout, ## note: for now always use knockout flag from round - why? why not??
655
+ ground_id: ground.present? ? ground.id : nil,
656
+ group_id: @group.present? ? @group.id : nil
657
+ }
658
+
659
+ game_attribs[ :pos ] = pos if pos.present?
660
+
661
+ ####
662
+ # note: only update if any changes (or create if new record)
663
+ if game.present? &&
664
+ game.check_for_changes( game_attribs ) == false
665
+ logger.debug " skip update game #{game.id}; no changes found"
666
+ else
667
+ if game.present?
668
+ logger.debug "update game #{game.id}:"
669
+ else
670
+ logger.debug "create game:"
671
+ game = Game.new
672
+
673
+ more_game_attribs = {
674
+ round_id: round.id,
675
+ team1_id: team1.id,
676
+ team2_id: team2.id
677
+ }
678
+
679
+ ## NB: use round.games.count for pos
680
+ ## lets us add games out of order if later needed
681
+ more_game_attribs[ :pos ] = round.games.count+1 if pos.nil?
682
+
683
+ game_attribs = game_attribs.merge( more_game_attribs )
684
+ end
685
+
686
+ logger.debug game_attribs.to_json
687
+ game.update_attributes!( game_attribs )
688
+ end
689
+
690
+ @last_game = game # store for later reference (e.g. used for goals etc.)
691
+
692
+ return true # game match found
693
+ end # method parse_game
694
+
695
+
696
+ def try_parse_date_header( line )
697
+ # note: clone line; for possible test do NOT modify in place for now
698
+ # note: returns true if parsed, false if no match
699
+ parse_date_header( line.dup )
700
+ end
701
+
702
+ def parse_date_header( line )
703
+ # note: returns true if parsed, false if no match
704
+
705
+ # line with NO teams plus include date e.g.
706
+ # [Fri Jun/17] or
707
+ # Jun/17 or
708
+ # Jun/17: etc.
709
+
710
+
711
+ @mapper_teams.map_teams!( line )
712
+ team_keys = @mapper_teams.find_teams!( line )
713
+ team1_key = team_keys[0]
714
+ team2_key = team_keys[1]
715
+
716
+ date = find_date!( line, start_at: @event.start_at )
717
+
718
+ if date && team1_key.nil? && team2_key.nil?
719
+ logger.debug( "date header line found: >#{line}<")
720
+ logger.debug( " date: #{date}")
721
+
722
+ @last_date = date # keep a reference for later use
723
+ return true
724
+ else
725
+ return false
726
+ end
727
+ end
728
+
729
+
730
+ def parse_goals( line )
731
+ logger.debug "parsing goals (fixture) line: >#{line}<"
732
+
733
+ goals = GoalsFinder.new.find!( line )
734
+
735
+ ## check if squads/rosters present for player mappings
736
+ #
737
+ squad1_count = Roster.where( event_id: @event.id, team_id: @last_team1 ).count
738
+ if squad1_count > 0
739
+ squad1 = Roster.where( event_id: @event.id, team_id: @last_team1 )
740
+ else
741
+ squad1 = []
742
+ end
743
+
744
+ squad2_count = Roster.where( event_id: @event.id, team_id: @last_team2 ).count
745
+ if squad2_count > 0
746
+ squad2 = Roster.where( event_id: @event.id, team_id: @last_team2 )
747
+ else
748
+ squad2 = []
749
+ end
750
+
751
+ #####
752
+ # todo/fix: try lookup by squads first!!!
753
+ # issue warning if player not included in squad!!
754
+
755
+ ##########
756
+ # try mapping player names to player
757
+
758
+ ## note: first delete all goals for match (and recreate new ones
759
+ # no need to figure out update/merge strategy)
760
+ @last_game.goals.delete_all
761
+
762
+
763
+ goals.each do |goal|
764
+ player_name = goal.name
765
+
766
+ player = Person.where( name: player_name ).first
767
+ if player
768
+ logger.info " player match (name eq) - using player key #{player.key}"
769
+ else
770
+ # try like match (player name might only include part of name e.g. Messi)
771
+ # try three variants
772
+ # try %Messi
773
+ # try Messi%
774
+ # try %Messi% -- check if there's an easier way w/ "one" where clause?
775
+ player = Person.where( 'name LIKE ? OR name LIKE ? OR name LIKE ?',
776
+ "%#{player_name}",
777
+ "#{player_name}%",
778
+ "%#{player_name}%"
779
+ ).first
780
+
781
+ if player
782
+ logger.info " player match (name like) - using player key #{player.key}"
783
+ else
784
+ # try synonyms
785
+ player = Person.where( 'synonyms LIKE ? OR synonyms LIKE ? OR synonyms LIKE ?',
786
+ "%#{player_name}",
787
+ "#{player_name}%",
788
+ "%#{player_name}%"
789
+ ).first
790
+ if player
791
+ logger.info " player match (synonyms like) - using player key #{player.key}"
792
+ else
793
+ # auto-create player (player not found)
794
+ logger.info " player NOT found >#{player_name}< - auto-create"
795
+
796
+ ## fix: add auto flag (for auto-created persons/players)
797
+ ## fix: move title_to_key logic to person model etc.
798
+ player_key = TextUtils.title_to_key( player_name )
799
+ player_attribs = {
800
+ key: player_key,
801
+ title: player_name
802
+ }
803
+ logger.info " using attribs: #{player_attribs.inspect}"
804
+
805
+ player = Person.create!( player_attribs )
806
+ end
807
+ end
808
+ end
809
+
810
+ goal_attribs = {
811
+ game_id: @last_game.id,
812
+ team_id: goal.team == 1 ? @last_team1.id : @last_team2.id,
813
+ person_id: player.id,
814
+ minute: goal.minute,
815
+ offset: goal.offset,
816
+ penalty: goal.penalty,
817
+ owngoal: goal.owngoal,
818
+ score1: goal.score1,
819
+ score2: goal.score2
820
+ }
821
+
822
+ logger.info " adding goal using attribs: #{goal_attribs.inspect}"
823
+ Goal.create!( goal_attribs )
824
+ end # each goals
825
+
826
+ end # method parse_goals
827
+
828
+
829
+ =begin
830
+ ###### add to person and use!!!
831
+ def self.create_or_update_from_values( values, more_attribs={} )
832
+ ## key & title required
833
+
834
+ attribs, more_values = find_key_n_title( values )
835
+ attribs = attribs.merge( more_attribs )
836
+
837
+ ## check for optional values
838
+ Person.create_or_update_from_attribs( attribs, more_values )
839
+ end
840
+ =end
841
+
842
+
843
+ def parse_fixtures( reader )
844
+
845
+ reader.each_line do |line|
846
+
847
+ if is_goals?( line )
848
+ parse_goals( line )
849
+ elsif is_round_def?( line )
850
+ ## todo/fix: add round definition (w begin n end date)
851
+ ## todo: do not patch rounds with definition (already assume begin/end date is good)
852
+ ## -- how to deal with matches that get rescheduled/postponed?
853
+ parse_round_def( line )
854
+ elsif is_round?( line )
855
+ parse_round_header( line )
856
+ elsif is_group_def?( line ) ## NB: group goes after round (round may contain group marker too)
857
+ ### todo: add pipe (|) marker (required)
858
+ parse_group_def( line )
859
+ elsif is_group?( line )
860
+ ## -- lets you set group e.g. Group A etc.
861
+ parse_group_header( line )
862
+ elsif try_parse_game( line )
863
+ # do nothing here
864
+ elsif try_parse_date_header( line )
865
+ # do nothing here
866
+ else
867
+ logger.info "skipping line (no match found): >#{line}<"
868
+ end
869
+ end # lines.each
870
+
871
+ ###########################
872
+ # backtrack and patch round pos and round dates (start_at/end_at)
873
+ # note: patch dates must go first! (otherwise sort_by_date will not work for round pos)
874
+
875
+ unless @patch_round_ids_dates.empty?
876
+ ###
877
+ # fix: do NOT patch if auto flag is set to false !!!
878
+ # e.g. rounds got added w/ round def (not w/ round header)
879
+
880
+ # note: use uniq - to allow multiple round headers (possible?)
881
+
882
+ Round.find( @patch_round_ids_dates.uniq ).each do |r|
883
+ logger.debug "patch round start_at/end_at date for #{r.title}:"
884
+
885
+ ## note:
886
+ ## will add "scope" pos first e.g
887
+ #
888
+ ## SELECT "games".* FROM "games" WHERE "games"."round_id" = ?
889
+ # ORDER BY pos, play_at asc [["round_id", 7]]
890
+ # thus will NOT order by play_at but by pos first!!!
891
+ # =>
892
+ # need to unscope pos!!! or use unordered_games - games_by_play_at_date etc.??
893
+ # thus use reorder()!!! - not just order('play_at asc')
894
+
895
+ games = r.games.reorder( 'play_at asc' ).all
896
+
897
+ ## skip rounds w/ no games
898
+
899
+ ## todo/check/fix: what's the best way for checking assoc w/ 0 recs?
900
+ next if games.size == 0
901
+
902
+ # note: make sure start_at/end_at is date only (e.g. use play_at.to_date)
903
+ # sqlite3 saves datetime in date field as datetime, for example (will break date compares later!)
904
+
905
+ round_attribs = {
906
+ start_at: games[0].play_at.to_date, # use games.first ?
907
+ end_at: games[-1].play_at.to_date # use games.last ? why? why not?
908
+ }
909
+
910
+ logger.debug round_attribs.to_json
911
+ r.update_attributes!( round_attribs )
912
+ end
913
+ end
914
+
915
+ unless @patch_round_ids_pos.empty?
916
+
917
+ # step 0: check for offset (last_round_pos)
918
+ if @last_round_pos
919
+ offset = @last_round_pos
920
+ logger.info " +++ patch round pos - use offset; start w/ #{offset}"
921
+ else
922
+ offset = 0
923
+ logger.debug " patch round pos - no offset; start w/ 0"
924
+ end
925
+
926
+ # step 1: sort by date
927
+ # step 2: update pos
928
+ # note: use uniq - to allow multiple round headers (possible?)
929
+ Round.order( 'start_at asc').find( @patch_round_ids_pos.uniq ).each_with_index do |r,idx|
930
+ # note: starts counting w/ zero(0)
931
+ logger.debug "[#{idx+1}] patch round pos >#{offset+idx+1}< for #{r.title}:"
932
+ round_attribs = {
933
+ pos: offset+idx+1
934
+ }
935
+
936
+ # update title if Matchday XXXX e.g. use Matchday 1 etc.
937
+ if r.title.starts_with?('Matchday')
938
+ round_attribs[:title] = "Matchday #{offset+idx+1}"
939
+ end
940
+
941
+ logger.debug round_attribs.to_json
942
+ r.update_attributes!( round_attribs )
943
+
944
+ # update last_round_pos offset too
945
+ @last_round_pos = [offset+idx+1,@last_round_pos||0].max
946
+ end
947
+ end
948
+
949
+ end # method parse_fixtures
950
+
951
+ end # class GameReader
952
+ end # module SportDb