cricinfo-cli-go 0.1.1 → 0.1.4
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.
- package/internal/cli/matches.go +126 -2
- package/internal/cli/matches_test.go +82 -0
- package/internal/cli/search.go +156 -0
- package/internal/cricinfo/analysis.go +393 -93
- package/internal/cricinfo/analysis_phase15_test.go +38 -0
- package/internal/cricinfo/client.go +23 -2
- package/internal/cricinfo/coverage_ledger_test.go +2 -22
- package/internal/cricinfo/entity_index.go +27 -0
- package/internal/cricinfo/historical_hydration.go +82 -42
- package/internal/cricinfo/matches.go +1641 -88
- package/internal/cricinfo/matches_phase7_test.go +11 -4
- package/internal/cricinfo/normalize_entities.go +83 -35
- package/internal/cricinfo/players.go +236 -2
- package/internal/cricinfo/render_contract.go +191 -49
- package/internal/cricinfo/renderer.go +613 -19
- package/internal/cricinfo/resolver.go +134 -13
- package/internal/cricinfo/teams.go +109 -6
- package/internal/cricinfo/testdata/coverage/cricinfo-field-path-catalog.txt +2536 -0
- package/internal/cricinfo/testdata/coverage/cricinfo-working-templates.tsv +56 -0
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ const (
|
|
|
19
19
|
analysisMetricSixesConceded = "sixes-conceded"
|
|
20
20
|
analysisMetricFours = "fours"
|
|
21
21
|
analysisMetricSixes = "sixes"
|
|
22
|
+
analysisMetricRuns = "runs"
|
|
22
23
|
analysisMetricStrikeRate = "strike-rate"
|
|
23
24
|
)
|
|
24
25
|
|
|
@@ -349,54 +350,62 @@ func (s *AnalysisService) Bowling(ctx context.Context, opts AnalysisMetricOption
|
|
|
349
350
|
|
|
350
351
|
agg := map[string]*analysisAggregate{}
|
|
351
352
|
warnings := append([]string{}, run.warnings...)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if statusErr := analysisTransportResult(EntityAnalysisBowl, match.ID, hydrateErr); statusErr != nil {
|
|
357
|
-
return *statusErr, nil
|
|
358
|
-
}
|
|
359
|
-
warnings = append(warnings, fmt.Sprintf("match %s player summary: %v", match.ID, hydrateErr))
|
|
360
|
-
continue
|
|
353
|
+
if run.mode == analysisScopeMatch {
|
|
354
|
+
if fastAgg, fastWarnings, used := s.hydrateMatchScopeBowlingFromScorecard(ctx, run, filters, groupBy); used {
|
|
355
|
+
agg = fastAgg
|
|
356
|
+
warnings = append(warnings, fastWarnings...)
|
|
361
357
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for _,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
TeamID: strings.TrimSpace(player.TeamID),
|
|
373
|
-
TeamName: strings.TrimSpace(teamName),
|
|
374
|
-
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
375
|
-
PlayerName: strings.TrimSpace(playerName),
|
|
376
|
-
CountValue: 1,
|
|
377
|
-
Dots: totals.dots,
|
|
378
|
-
SixesConceded: totals.sixesConceded,
|
|
379
|
-
Balls: totals.balls,
|
|
380
|
-
RunsConceded: totals.conceded,
|
|
381
|
-
EconomySample: totals.economy,
|
|
382
|
-
}
|
|
383
|
-
if !filters.matches(row) {
|
|
358
|
+
}
|
|
359
|
+
if len(agg) == 0 {
|
|
360
|
+
for _, match := range run.session.ScopedMatches() {
|
|
361
|
+
seasonID := seasonForMatch(match, run.seasonHint)
|
|
362
|
+
players, playerWarnings, hydrateErr := run.session.HydratePlayerMatchSummaries(ctx, match.ID)
|
|
363
|
+
if hydrateErr != nil {
|
|
364
|
+
if statusErr := analysisTransportResult(EntityAnalysisBowl, match.ID, hydrateErr); statusErr != nil {
|
|
365
|
+
return *statusErr, nil
|
|
366
|
+
}
|
|
367
|
+
warnings = append(warnings, fmt.Sprintf("match %s player summary: %v", match.ID, hydrateErr))
|
|
384
368
|
continue
|
|
385
369
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
370
|
+
warnings = append(warnings, playerWarnings...)
|
|
371
|
+
|
|
372
|
+
for _, player := range players {
|
|
373
|
+
totals := extractBowlingTotals(player)
|
|
374
|
+
playerName := analysisDisplayPlayerName(s.resolver, player.PlayerID, player.PlayerName)
|
|
375
|
+
teamName := analysisDisplayTeamName(s.resolver, player.TeamID, player.TeamName)
|
|
376
|
+
row := analysisSourceRow{
|
|
377
|
+
MatchID: strings.TrimSpace(player.MatchID),
|
|
378
|
+
LeagueID: strings.TrimSpace(player.LeagueID),
|
|
379
|
+
SeasonID: seasonID,
|
|
380
|
+
TeamID: strings.TrimSpace(player.TeamID),
|
|
381
|
+
TeamName: strings.TrimSpace(teamName),
|
|
382
|
+
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
383
|
+
PlayerName: strings.TrimSpace(playerName),
|
|
384
|
+
CountValue: 1,
|
|
385
|
+
Dots: totals.dots,
|
|
386
|
+
SixesConceded: totals.sixesConceded,
|
|
387
|
+
Balls: totals.balls,
|
|
388
|
+
RunsConceded: totals.conceded,
|
|
389
|
+
EconomySample: totals.economy,
|
|
390
|
+
}
|
|
391
|
+
if !filters.matches(row) {
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
key, dims := buildAnalysisGroup(row, groupBy)
|
|
395
|
+
entry := agg[key]
|
|
396
|
+
if entry == nil {
|
|
397
|
+
entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
|
|
398
|
+
agg[key] = entry
|
|
399
|
+
}
|
|
400
|
+
entry.matchIDs[row.MatchID] = struct{}{}
|
|
401
|
+
entry.dots += row.Dots
|
|
402
|
+
entry.sixesConceded += row.SixesConceded
|
|
403
|
+
entry.balls += row.Balls
|
|
404
|
+
entry.runsConceded += row.RunsConceded
|
|
405
|
+
if row.EconomySample > 0 {
|
|
406
|
+
entry.economyTotal += row.EconomySample
|
|
407
|
+
entry.economyCount++
|
|
408
|
+
}
|
|
400
409
|
}
|
|
401
410
|
}
|
|
402
411
|
}
|
|
@@ -447,6 +456,103 @@ func (s *AnalysisService) Bowling(ctx context.Context, opts AnalysisMetricOption
|
|
|
447
456
|
return analysisResult(EntityAnalysisBowl, view, warnings), nil
|
|
448
457
|
}
|
|
449
458
|
|
|
459
|
+
func (s *AnalysisService) hydrateMatchScopeBowlingFromScorecard(
|
|
460
|
+
ctx context.Context,
|
|
461
|
+
run *analysisScopeRun,
|
|
462
|
+
filters analysisFilterSpec,
|
|
463
|
+
groupBy []string,
|
|
464
|
+
) (map[string]*analysisAggregate, []string, bool) {
|
|
465
|
+
if run == nil || run.session == nil {
|
|
466
|
+
return nil, nil, false
|
|
467
|
+
}
|
|
468
|
+
matches := run.session.ScopedMatches()
|
|
469
|
+
if len(matches) != 1 {
|
|
470
|
+
return nil, nil, false
|
|
471
|
+
}
|
|
472
|
+
match := matches[0]
|
|
473
|
+
scorecardRef := matchSubresourceRef(match, "matchcards", "matchcards")
|
|
474
|
+
if strings.TrimSpace(scorecardRef) == "" {
|
|
475
|
+
return nil, nil, false
|
|
476
|
+
}
|
|
477
|
+
resolved, err := s.client.ResolveRefChain(ctx, scorecardRef)
|
|
478
|
+
if err != nil {
|
|
479
|
+
return nil, nil, false
|
|
480
|
+
}
|
|
481
|
+
scorecard, err := NormalizeMatchScorecard(resolved.Body, match)
|
|
482
|
+
if err != nil {
|
|
483
|
+
return nil, nil, false
|
|
484
|
+
}
|
|
485
|
+
if len(scorecard.BowlingCards) == 0 {
|
|
486
|
+
return nil, nil, false
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
teamIDByAlias := map[string]string{}
|
|
490
|
+
for _, team := range match.Teams {
|
|
491
|
+
teamID := strings.TrimSpace(team.ID)
|
|
492
|
+
for _, raw := range []string{team.Name, team.ShortName, team.Abbreviation} {
|
|
493
|
+
alias := normalizeAlias(raw)
|
|
494
|
+
if alias == "" {
|
|
495
|
+
continue
|
|
496
|
+
}
|
|
497
|
+
if _, exists := teamIDByAlias[alias]; !exists {
|
|
498
|
+
teamIDByAlias[alias] = teamID
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
seasonID := seasonForMatch(match, run.seasonHint)
|
|
504
|
+
agg := map[string]*analysisAggregate{}
|
|
505
|
+
for _, card := range scorecard.BowlingCards {
|
|
506
|
+
teamName := strings.TrimSpace(card.TeamName)
|
|
507
|
+
teamID := teamIDByAlias[normalizeAlias(teamName)]
|
|
508
|
+
teamName = analysisDisplayTeamName(s.resolver, teamID, teamName)
|
|
509
|
+
|
|
510
|
+
for _, player := range card.Players {
|
|
511
|
+
conceded := analysisNumericString(player.Conceded)
|
|
512
|
+
wickets := analysisNumericString(player.Wickets)
|
|
513
|
+
balls := oversToBalls(player.Overs)
|
|
514
|
+
econ := 0.0
|
|
515
|
+
if balls > 0 {
|
|
516
|
+
econ = float64(conceded) / (float64(balls) / 6.0)
|
|
517
|
+
}
|
|
518
|
+
row := analysisSourceRow{
|
|
519
|
+
MatchID: strings.TrimSpace(match.ID),
|
|
520
|
+
LeagueID: strings.TrimSpace(nonEmpty(match.LeagueID, run.scope.League.ID)),
|
|
521
|
+
SeasonID: seasonID,
|
|
522
|
+
TeamID: teamID,
|
|
523
|
+
TeamName: teamName,
|
|
524
|
+
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
525
|
+
PlayerName: analysisDisplayPlayerName(s.resolver, player.PlayerID, player.PlayerName),
|
|
526
|
+
CountValue: 1,
|
|
527
|
+
Balls: balls,
|
|
528
|
+
RunsConceded: conceded,
|
|
529
|
+
EconomySample: econ,
|
|
530
|
+
}
|
|
531
|
+
if wickets > 0 {
|
|
532
|
+
row.Dots = 0
|
|
533
|
+
}
|
|
534
|
+
if !filters.matches(row) {
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
key, dims := buildAnalysisGroup(row, groupBy)
|
|
539
|
+
entry := agg[key]
|
|
540
|
+
if entry == nil {
|
|
541
|
+
entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
|
|
542
|
+
agg[key] = entry
|
|
543
|
+
}
|
|
544
|
+
entry.matchIDs[row.MatchID] = struct{}{}
|
|
545
|
+
entry.balls += row.Balls
|
|
546
|
+
entry.runsConceded += row.RunsConceded
|
|
547
|
+
if row.EconomySample > 0 {
|
|
548
|
+
entry.economyTotal += row.EconomySample
|
|
549
|
+
entry.economyCount++
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return agg, nil, true
|
|
554
|
+
}
|
|
555
|
+
|
|
450
556
|
// Batting ranks batting metrics over match or season scope.
|
|
451
557
|
func (s *AnalysisService) Batting(ctx context.Context, opts AnalysisMetricOptions) (NormalizedResult, error) {
|
|
452
558
|
metric, err := normalizeBattingMetric(opts.Metric)
|
|
@@ -467,54 +573,62 @@ func (s *AnalysisService) Batting(ctx context.Context, opts AnalysisMetricOption
|
|
|
467
573
|
|
|
468
574
|
agg := map[string]*analysisAggregate{}
|
|
469
575
|
warnings := append([]string{}, run.warnings...)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if statusErr := analysisTransportResult(EntityAnalysisBat, match.ID, hydrateErr); statusErr != nil {
|
|
475
|
-
return *statusErr, nil
|
|
476
|
-
}
|
|
477
|
-
warnings = append(warnings, fmt.Sprintf("match %s player summary: %v", match.ID, hydrateErr))
|
|
478
|
-
continue
|
|
576
|
+
if run.mode == analysisScopeMatch {
|
|
577
|
+
if fastAgg, fastWarnings, used := s.hydrateMatchScopeBattingFromScorecard(ctx, run, filters, groupBy); used {
|
|
578
|
+
agg = fastAgg
|
|
579
|
+
warnings = append(warnings, fastWarnings...)
|
|
479
580
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
for _,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
TeamID: strings.TrimSpace(player.TeamID),
|
|
491
|
-
TeamName: strings.TrimSpace(teamName),
|
|
492
|
-
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
493
|
-
PlayerName: strings.TrimSpace(playerName),
|
|
494
|
-
CountValue: 1,
|
|
495
|
-
Fours: totals.fours,
|
|
496
|
-
BattingSixes: totals.sixes,
|
|
497
|
-
RunsScored: totals.runs,
|
|
498
|
-
BallsFaced: totals.balls,
|
|
499
|
-
StrikeSample: totals.strikeRate,
|
|
500
|
-
}
|
|
501
|
-
if !filters.matches(row) {
|
|
581
|
+
}
|
|
582
|
+
if len(agg) == 0 {
|
|
583
|
+
for _, match := range run.session.ScopedMatches() {
|
|
584
|
+
seasonID := seasonForMatch(match, run.seasonHint)
|
|
585
|
+
players, playerWarnings, hydrateErr := run.session.HydratePlayerMatchSummaries(ctx, match.ID)
|
|
586
|
+
if hydrateErr != nil {
|
|
587
|
+
if statusErr := analysisTransportResult(EntityAnalysisBat, match.ID, hydrateErr); statusErr != nil {
|
|
588
|
+
return *statusErr, nil
|
|
589
|
+
}
|
|
590
|
+
warnings = append(warnings, fmt.Sprintf("match %s player summary: %v", match.ID, hydrateErr))
|
|
502
591
|
continue
|
|
503
592
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
593
|
+
warnings = append(warnings, playerWarnings...)
|
|
594
|
+
|
|
595
|
+
for _, player := range players {
|
|
596
|
+
totals := extractBattingTotals(player)
|
|
597
|
+
playerName := analysisDisplayPlayerName(s.resolver, player.PlayerID, player.PlayerName)
|
|
598
|
+
teamName := analysisDisplayTeamName(s.resolver, player.TeamID, player.TeamName)
|
|
599
|
+
row := analysisSourceRow{
|
|
600
|
+
MatchID: strings.TrimSpace(player.MatchID),
|
|
601
|
+
LeagueID: strings.TrimSpace(player.LeagueID),
|
|
602
|
+
SeasonID: seasonID,
|
|
603
|
+
TeamID: strings.TrimSpace(player.TeamID),
|
|
604
|
+
TeamName: strings.TrimSpace(teamName),
|
|
605
|
+
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
606
|
+
PlayerName: strings.TrimSpace(playerName),
|
|
607
|
+
CountValue: 1,
|
|
608
|
+
Fours: totals.fours,
|
|
609
|
+
BattingSixes: totals.sixes,
|
|
610
|
+
RunsScored: totals.runs,
|
|
611
|
+
BallsFaced: totals.balls,
|
|
612
|
+
StrikeSample: totals.strikeRate,
|
|
613
|
+
}
|
|
614
|
+
if !filters.matches(row) {
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
key, dims := buildAnalysisGroup(row, groupBy)
|
|
618
|
+
entry := agg[key]
|
|
619
|
+
if entry == nil {
|
|
620
|
+
entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
|
|
621
|
+
agg[key] = entry
|
|
622
|
+
}
|
|
623
|
+
entry.matchIDs[row.MatchID] = struct{}{}
|
|
624
|
+
entry.fours += row.Fours
|
|
625
|
+
entry.battingSixes += row.BattingSixes
|
|
626
|
+
entry.runsScored += row.RunsScored
|
|
627
|
+
entry.ballsFaced += row.BallsFaced
|
|
628
|
+
if row.StrikeSample > 0 {
|
|
629
|
+
entry.strikeRateTotal += row.StrikeSample
|
|
630
|
+
entry.strikeRateCount++
|
|
631
|
+
}
|
|
518
632
|
}
|
|
519
633
|
}
|
|
520
634
|
}
|
|
@@ -533,6 +647,9 @@ func (s *AnalysisService) Batting(ctx context.Context, opts AnalysisMetricOption
|
|
|
533
647
|
case analysisMetricSixes:
|
|
534
648
|
row.Value = float64(entry.battingSixes)
|
|
535
649
|
row.Count = entry.battingSixes
|
|
650
|
+
case analysisMetricRuns:
|
|
651
|
+
row.Value = float64(entry.runsScored)
|
|
652
|
+
row.Count = entry.runsScored
|
|
536
653
|
case analysisMetricStrikeRate:
|
|
537
654
|
row.Value = strikeRateFromAggregate(entry)
|
|
538
655
|
}
|
|
@@ -559,6 +676,133 @@ func (s *AnalysisService) Batting(ctx context.Context, opts AnalysisMetricOption
|
|
|
559
676
|
return analysisResult(EntityAnalysisBat, view, warnings), nil
|
|
560
677
|
}
|
|
561
678
|
|
|
679
|
+
func (s *AnalysisService) hydrateMatchScopeBattingFromScorecard(
|
|
680
|
+
ctx context.Context,
|
|
681
|
+
run *analysisScopeRun,
|
|
682
|
+
filters analysisFilterSpec,
|
|
683
|
+
groupBy []string,
|
|
684
|
+
) (map[string]*analysisAggregate, []string, bool) {
|
|
685
|
+
if run == nil || run.session == nil {
|
|
686
|
+
return nil, nil, false
|
|
687
|
+
}
|
|
688
|
+
matches := run.session.ScopedMatches()
|
|
689
|
+
if len(matches) != 1 {
|
|
690
|
+
return nil, nil, false
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
match := matches[0]
|
|
694
|
+
scorecardRef := matchSubresourceRef(match, "matchcards", "matchcards")
|
|
695
|
+
if strings.TrimSpace(scorecardRef) == "" {
|
|
696
|
+
return nil, nil, false
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
resolved, err := s.client.ResolveRefChain(ctx, scorecardRef)
|
|
700
|
+
if err != nil {
|
|
701
|
+
return nil, nil, false
|
|
702
|
+
}
|
|
703
|
+
scorecard, err := NormalizeMatchScorecard(resolved.Body, match)
|
|
704
|
+
if err != nil {
|
|
705
|
+
return nil, nil, false
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
teamIDByAlias := map[string]string{}
|
|
709
|
+
playerNameByID := map[string]string{}
|
|
710
|
+
for _, team := range match.Teams {
|
|
711
|
+
teamID := strings.TrimSpace(team.ID)
|
|
712
|
+
for _, raw := range []string{team.Name, team.ShortName, team.Abbreviation} {
|
|
713
|
+
alias := normalizeAlias(raw)
|
|
714
|
+
if alias == "" {
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
if _, exists := teamIDByAlias[alias]; !exists {
|
|
718
|
+
teamIDByAlias[alias] = teamID
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
rosterRef := nonEmpty(strings.TrimSpace(team.RosterRef), competitorSubresourceRef(match, team.ID, "roster"))
|
|
723
|
+
if strings.TrimSpace(rosterRef) == "" {
|
|
724
|
+
continue
|
|
725
|
+
}
|
|
726
|
+
rosterDoc, rosterErr := s.client.ResolveRefChain(ctx, rosterRef)
|
|
727
|
+
if rosterErr != nil {
|
|
728
|
+
continue
|
|
729
|
+
}
|
|
730
|
+
entries, normalizeErr := NormalizeTeamRosterEntries(rosterDoc.Body, team, TeamScopeMatch, match.ID)
|
|
731
|
+
if normalizeErr != nil {
|
|
732
|
+
continue
|
|
733
|
+
}
|
|
734
|
+
for _, entry := range entries {
|
|
735
|
+
playerID := strings.TrimSpace(entry.PlayerID)
|
|
736
|
+
playerName := strings.TrimSpace(entry.DisplayName)
|
|
737
|
+
if playerID == "" || playerName == "" {
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
740
|
+
if _, exists := playerNameByID[playerID]; !exists {
|
|
741
|
+
playerNameByID[playerID] = playerName
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
seasonID := seasonForMatch(match, run.seasonHint)
|
|
747
|
+
agg := map[string]*analysisAggregate{}
|
|
748
|
+
for _, card := range scorecard.BattingCards {
|
|
749
|
+
teamName := strings.TrimSpace(card.TeamName)
|
|
750
|
+
teamID := teamIDByAlias[normalizeAlias(teamName)]
|
|
751
|
+
teamName = analysisDisplayTeamName(s.resolver, teamID, teamName)
|
|
752
|
+
|
|
753
|
+
for _, player := range card.Players {
|
|
754
|
+
runs := analysisNumericString(player.Runs)
|
|
755
|
+
balls := analysisNumericString(player.BallsFaced)
|
|
756
|
+
fours := analysisNumericString(player.Fours)
|
|
757
|
+
sixes := analysisNumericString(player.Sixes)
|
|
758
|
+
strikeRate := 0.0
|
|
759
|
+
if balls > 0 {
|
|
760
|
+
strikeRate = (float64(runs) * 100.0) / float64(balls)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
row := analysisSourceRow{
|
|
764
|
+
MatchID: strings.TrimSpace(match.ID),
|
|
765
|
+
LeagueID: strings.TrimSpace(nonEmpty(match.LeagueID, run.scope.League.ID)),
|
|
766
|
+
SeasonID: seasonID,
|
|
767
|
+
TeamID: teamID,
|
|
768
|
+
TeamName: teamName,
|
|
769
|
+
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
770
|
+
PlayerName: analysisDisplayPlayerName(
|
|
771
|
+
s.resolver,
|
|
772
|
+
player.PlayerID,
|
|
773
|
+
nonEmpty(playerNameByID[strings.TrimSpace(player.PlayerID)], player.PlayerName),
|
|
774
|
+
),
|
|
775
|
+
CountValue: 1,
|
|
776
|
+
Fours: fours,
|
|
777
|
+
BattingSixes: sixes,
|
|
778
|
+
RunsScored: runs,
|
|
779
|
+
BallsFaced: balls,
|
|
780
|
+
StrikeSample: strikeRate,
|
|
781
|
+
}
|
|
782
|
+
if !filters.matches(row) {
|
|
783
|
+
continue
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
key, dims := buildAnalysisGroup(row, groupBy)
|
|
787
|
+
entry := agg[key]
|
|
788
|
+
if entry == nil {
|
|
789
|
+
entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
|
|
790
|
+
agg[key] = entry
|
|
791
|
+
}
|
|
792
|
+
entry.matchIDs[row.MatchID] = struct{}{}
|
|
793
|
+
entry.fours += row.Fours
|
|
794
|
+
entry.battingSixes += row.BattingSixes
|
|
795
|
+
entry.runsScored += row.RunsScored
|
|
796
|
+
entry.ballsFaced += row.BallsFaced
|
|
797
|
+
if row.StrikeSample > 0 {
|
|
798
|
+
entry.strikeRateTotal += row.StrikeSample
|
|
799
|
+
entry.strikeRateCount++
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return agg, nil, true
|
|
804
|
+
}
|
|
805
|
+
|
|
562
806
|
// Partnerships ranks partnerships over match or season scope.
|
|
563
807
|
func (s *AnalysisService) Partnerships(ctx context.Context, opts AnalysisMetricOptions) (NormalizedResult, error) {
|
|
564
808
|
groupBy, err := parseGroupBy(opts.GroupBy, []string{"innings"}, analysisGroupPartnershipAllowed)
|
|
@@ -858,10 +1102,10 @@ func normalizeBattingMetric(raw string) (string, error) {
|
|
|
858
1102
|
metric = strings.ReplaceAll(metric, "_", "-")
|
|
859
1103
|
metric = strings.ReplaceAll(metric, " ", "-")
|
|
860
1104
|
switch metric {
|
|
861
|
-
case analysisMetricFours, analysisMetricSixes, analysisMetricStrikeRate:
|
|
1105
|
+
case analysisMetricFours, analysisMetricSixes, analysisMetricRuns, analysisMetricStrikeRate:
|
|
862
1106
|
return metric, nil
|
|
863
1107
|
default:
|
|
864
|
-
return "", fmt.Errorf("--metric must be one of: fours, sixes, strike-rate")
|
|
1108
|
+
return "", fmt.Errorf("--metric must be one of: fours, sixes, runs, strike-rate")
|
|
865
1109
|
}
|
|
866
1110
|
}
|
|
867
1111
|
|
|
@@ -1191,7 +1435,7 @@ type battingTotals struct {
|
|
|
1191
1435
|
}
|
|
1192
1436
|
|
|
1193
1437
|
func extractBattingTotals(player PlayerMatch) battingTotals {
|
|
1194
|
-
totals := battingTotals{strikeRate: player.Summary.StrikeRate
|
|
1438
|
+
totals := battingTotals{strikeRate: player.Summary.StrikeRate}
|
|
1195
1439
|
for _, category := range player.Batting {
|
|
1196
1440
|
for _, stat := range category.Stats {
|
|
1197
1441
|
switch normalizeStatName(stat.Name) {
|
|
@@ -1210,6 +1454,12 @@ func extractBattingTotals(player PlayerMatch) battingTotals {
|
|
|
1210
1454
|
}
|
|
1211
1455
|
}
|
|
1212
1456
|
}
|
|
1457
|
+
|
|
1458
|
+
// Batting summary fields are frequently derived from the same stats payload.
|
|
1459
|
+
// Prefer the larger value rather than summing to avoid duplicate counting.
|
|
1460
|
+
if player.Summary.BallsFaced > 0 {
|
|
1461
|
+
totals.balls = analysisMaxInt(totals.balls, player.Summary.BallsFaced)
|
|
1462
|
+
}
|
|
1213
1463
|
return totals
|
|
1214
1464
|
}
|
|
1215
1465
|
|
|
@@ -1267,6 +1517,56 @@ func analysisMaxInt(a, b int) int {
|
|
|
1267
1517
|
return b
|
|
1268
1518
|
}
|
|
1269
1519
|
|
|
1520
|
+
func analysisNumericString(raw string) int {
|
|
1521
|
+
raw = strings.TrimSpace(raw)
|
|
1522
|
+
if raw == "" {
|
|
1523
|
+
return 0
|
|
1524
|
+
}
|
|
1525
|
+
start := -1
|
|
1526
|
+
end := -1
|
|
1527
|
+
for i, r := range raw {
|
|
1528
|
+
if r >= '0' && r <= '9' {
|
|
1529
|
+
if start == -1 {
|
|
1530
|
+
start = i
|
|
1531
|
+
}
|
|
1532
|
+
end = i + 1
|
|
1533
|
+
continue
|
|
1534
|
+
}
|
|
1535
|
+
if start != -1 {
|
|
1536
|
+
break
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if start == -1 || end == -1 {
|
|
1540
|
+
return 0
|
|
1541
|
+
}
|
|
1542
|
+
value, err := strconv.Atoi(raw[start:end])
|
|
1543
|
+
if err != nil {
|
|
1544
|
+
return 0
|
|
1545
|
+
}
|
|
1546
|
+
return value
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
func oversToBalls(raw string) int {
|
|
1550
|
+
raw = strings.TrimSpace(raw)
|
|
1551
|
+
if raw == "" {
|
|
1552
|
+
return 0
|
|
1553
|
+
}
|
|
1554
|
+
if strings.Contains(raw, ".") {
|
|
1555
|
+
parts := strings.SplitN(raw, ".", 2)
|
|
1556
|
+
whole, errWhole := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
1557
|
+
frac, errFrac := strconv.Atoi(strings.TrimSpace(parts[1]))
|
|
1558
|
+
if errWhole != nil || errFrac != nil || whole < 0 || frac < 0 || frac > 6 {
|
|
1559
|
+
return 0
|
|
1560
|
+
}
|
|
1561
|
+
return whole*6 + frac
|
|
1562
|
+
}
|
|
1563
|
+
value, err := strconv.Atoi(raw)
|
|
1564
|
+
if err != nil || value < 0 {
|
|
1565
|
+
return 0
|
|
1566
|
+
}
|
|
1567
|
+
return value * 6
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1270
1570
|
func analysisDisplayPlayerName(resolver *Resolver, playerID, fallback string) string {
|
|
1271
1571
|
name := strings.TrimSpace(fallback)
|
|
1272
1572
|
if name != "" {
|
|
@@ -203,6 +203,44 @@ func TestPhase15BowlingActivityFilterSkipsNonBowlers(t *testing.T) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
func TestPhase15ExtractBattingTotalsDoesNotDoubleCountSummaryBalls(t *testing.T) {
|
|
207
|
+
t.Parallel()
|
|
208
|
+
|
|
209
|
+
player := PlayerMatch{
|
|
210
|
+
PlayerID: "1408688",
|
|
211
|
+
PlayerName: "Vaibhav Sooryavanshi",
|
|
212
|
+
Batting: []StatCategory{
|
|
213
|
+
{
|
|
214
|
+
Name: "general",
|
|
215
|
+
Stats: []StatValue{
|
|
216
|
+
{Name: "runs", Value: "78"},
|
|
217
|
+
{Name: "ballsFaced", Value: "26"},
|
|
218
|
+
{Name: "fours", Value: "8"},
|
|
219
|
+
{Name: "sixes", Value: "7"},
|
|
220
|
+
{Name: "strikeRate", Value: "300"},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
Summary: PlayerMatchSummary{
|
|
225
|
+
BallsFaced: 26,
|
|
226
|
+
StrikeRate: 300,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
totals := extractBattingTotals(player)
|
|
231
|
+
if totals.runs != 78 || totals.balls != 26 {
|
|
232
|
+
t.Fatalf("expected 78/26 batting totals, got runs=%d balls=%d", totals.runs, totals.balls)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
agg := &analysisAggregate{
|
|
236
|
+
runsScored: totals.runs,
|
|
237
|
+
ballsFaced: totals.balls,
|
|
238
|
+
}
|
|
239
|
+
if got := strikeRateFromAggregate(agg); got != 300 {
|
|
240
|
+
t.Fatalf("expected strike rate 300, got %.2f", got)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
206
244
|
func TestLivePhase15SmallHistoricalScope(t *testing.T) {
|
|
207
245
|
t.Parallel()
|
|
208
246
|
requireLiveMatrix(t)
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"io"
|
|
9
9
|
"math"
|
|
10
10
|
"math/rand"
|
|
11
|
+
"net"
|
|
11
12
|
"net/http"
|
|
12
13
|
"net/url"
|
|
13
14
|
"strings"
|
|
@@ -138,7 +139,18 @@ func NewClient(cfg Config) (*Client, error) {
|
|
|
138
139
|
|
|
139
140
|
httpClient := cfg.HTTPClient
|
|
140
141
|
if httpClient == nil {
|
|
141
|
-
httpClient = &http.Client{
|
|
142
|
+
httpClient = &http.Client{
|
|
143
|
+
Transport: &http.Transport{
|
|
144
|
+
Proxy: http.ProxyFromEnvironment,
|
|
145
|
+
DialContext: (&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
|
146
|
+
ForceAttemptHTTP2: true,
|
|
147
|
+
MaxIdleConns: 128,
|
|
148
|
+
MaxIdleConnsPerHost: 32,
|
|
149
|
+
IdleConnTimeout: 90 * time.Second,
|
|
150
|
+
TLSHandshakeTimeout: 5 * time.Second,
|
|
151
|
+
ExpectContinueTimeout: 1 * time.Second,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
sleep := cfg.Sleep
|
|
@@ -207,7 +219,7 @@ func (c *Client) Fetch(ctx context.Context, ref string) (*Document, error) {
|
|
|
207
219
|
return nil, fmt.Errorf("read response %q: %w", requestURL, readErr)
|
|
208
220
|
}
|
|
209
221
|
|
|
210
|
-
if resp.StatusCode
|
|
222
|
+
if c.shouldRetryStatus(resp.StatusCode) && attempt < c.maxRetries {
|
|
211
223
|
if sleepErr := c.sleep(ctx, c.retryDelay(attempt)); sleepErr != nil {
|
|
212
224
|
return nil, sleepErr
|
|
213
225
|
}
|
|
@@ -348,6 +360,15 @@ func (c *Client) shouldRetryError(err error, parentCtx context.Context) bool {
|
|
|
348
360
|
return parentCtx.Err() == nil
|
|
349
361
|
}
|
|
350
362
|
|
|
363
|
+
func (c *Client) shouldRetryStatus(statusCode int) bool {
|
|
364
|
+
switch statusCode {
|
|
365
|
+
case http.StatusRequestTimeout, http.StatusTooManyRequests:
|
|
366
|
+
return true
|
|
367
|
+
default:
|
|
368
|
+
return statusCode >= defaultRetryStatusCode
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
351
372
|
func (c *Client) retryDelay(attempt int) time.Duration {
|
|
352
373
|
delay := float64(c.retryBaseDelay)
|
|
353
374
|
delay *= math.Pow(2, float64(attempt))
|