sportdb-readers 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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