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.
@@ -6,6 +6,7 @@ import (
6
6
  "io"
7
7
  "regexp"
8
8
  "sort"
9
+ "strconv"
9
10
  "strings"
10
11
  )
11
12
 
@@ -201,6 +202,15 @@ func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error
201
202
  lines = append(lines, formatInningsTimelines(itemMap)...)
202
203
  return writeTextLines(w, lines)
203
204
  }
205
+ if result.Kind == EntityMatchSituation {
206
+ itemMap, err := toMap(result.Data, opts.AllFields)
207
+ if err != nil {
208
+ return err
209
+ }
210
+ lines = append(lines, "Match Situation")
211
+ lines = append(lines, formatMatchSituation(itemMap)...)
212
+ return writeTextLines(w, lines)
213
+ }
204
214
  if result.Kind == EntityPlayerStats {
205
215
  itemMap, err := toMap(result.Data, opts.AllFields)
206
216
  if err != nil {
@@ -255,6 +265,24 @@ func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error
255
265
  lines = append(lines, formatMatchView(itemMap)...)
256
266
  return writeTextLines(w, lines)
257
267
  }
268
+ if result.Kind == EntityMatchPhases {
269
+ itemMap, err := toMap(result.Data, opts.AllFields)
270
+ if err != nil {
271
+ return err
272
+ }
273
+ lines = append(lines, "Match Phases")
274
+ lines = append(lines, formatMatchPhases(itemMap)...)
275
+ return writeTextLines(w, lines)
276
+ }
277
+ if result.Kind == EntityMatchDuel {
278
+ itemMap, err := toMap(result.Data, opts.AllFields)
279
+ if err != nil {
280
+ return err
281
+ }
282
+ lines = append(lines, "Match Duel")
283
+ lines = append(lines, formatMatchDuel(itemMap)...)
284
+ return writeTextLines(w, lines)
285
+ }
258
286
 
259
287
  itemMap, err := toMap(result.Data, opts.AllFields)
260
288
  if err != nil {
@@ -274,13 +302,68 @@ func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error
274
302
  return writeTextLines(w, lines)
275
303
  }
276
304
 
277
- lines = append(lines, fmt.Sprintf("%s (%d)", titleize(kindPlural(result.Kind)), len(result.Items)))
278
- for i, item := range result.Items {
305
+ if result.Kind == EntityTeamStatistics || result.Kind == EntityTeamRecords {
306
+ title := "Team Statistics"
307
+ if result.Kind == EntityTeamRecords {
308
+ title = "Team Records"
309
+ }
310
+ lines = append(lines, fmt.Sprintf("%s Categories (%d)", title, len(result.Items)))
311
+ lines = append(lines, formatStatCategoryList(result.Items)...)
312
+ return writeTextLines(w, lines)
313
+ }
314
+
315
+ if result.Kind == EntityStandingsGroup {
316
+ lines = append(lines, formatStandingsGroupList(result.Items)...)
317
+ return writeTextLines(w, lines)
318
+ }
319
+
320
+ if result.Kind == EntityDeliveryEvent || result.Kind == EntityPlayerDelivery {
321
+ lines = append(lines, fmt.Sprintf("%s (%d)", titleize(kindPlural(result.Kind)), len(result.Items)))
322
+ if result.Kind == EntityDeliveryEvent {
323
+ source := ""
324
+ switch {
325
+ case strings.Contains(result.RequestedRef, "/details"):
326
+ source = "details"
327
+ case strings.Contains(result.RequestedRef, "/plays"):
328
+ source = "plays"
329
+ }
330
+ if source != "" {
331
+ lines = append(lines, "Source: "+source)
332
+ }
333
+ }
334
+ for i, item := range result.Items {
335
+ summary := summarizeDeliveryListItem(item, result.Kind)
336
+ if strings.TrimSpace(summary) == "" {
337
+ continue
338
+ }
339
+ lines = append(lines, fmt.Sprintf("%d. %s", i+1, summary))
340
+ }
341
+ return writeTextLines(w, lines)
342
+ }
343
+
344
+ summaries := make([]string, 0, len(result.Items))
345
+ for _, item := range result.Items {
279
346
  itemMap, err := toMap(item, opts.AllFields)
280
347
  if err != nil {
281
348
  return err
282
349
  }
283
- lines = append(lines, fmt.Sprintf("%d. %s", i+1, summarizeEntity(itemMap, result.Kind, opts.Verbose)))
350
+ summary := summarizeEntity(itemMap, result.Kind, opts.Verbose)
351
+ if strings.TrimSpace(summary) == "" {
352
+ continue
353
+ }
354
+ summaries = append(summaries, summary)
355
+ }
356
+ if len(summaries) == 0 {
357
+ message := result.Message
358
+ if message == "" {
359
+ message = fmt.Sprintf("No %s found.", kindPlural(result.Kind))
360
+ }
361
+ lines = append(lines, sentenceCase(message))
362
+ return writeTextLines(w, lines)
363
+ }
364
+ lines = append(lines, fmt.Sprintf("%s (%d)", titleize(kindPlural(result.Kind)), len(summaries)))
365
+ for i, summary := range summaries {
366
+ lines = append(lines, fmt.Sprintf("%d. %s", i+1, summary))
284
367
  }
285
368
 
286
369
  return writeTextLines(w, lines)
@@ -295,6 +378,71 @@ func writeTextLines(w io.Writer, lines []string) error {
295
378
  return nil
296
379
  }
297
380
 
381
+ func summarizeDeliveryListItem(item any, kind EntityKind) string {
382
+ switch typed := item.(type) {
383
+ case DeliveryEvent:
384
+ short := firstNonEmpty(strings.TrimSpace(typed.ShortText), strings.TrimSpace(typed.Text))
385
+ if short == "" {
386
+ short = joinParts("over "+intString(typed.OverNumber), "ball "+intString(typed.BallNumber))
387
+ }
388
+ lead := firstNonEmpty(overBallString(typed.OverNumber, typed.BallNumber), "")
389
+ score := firstNonEmpty(scoreLabel(typed.HomeScore), scoreLabel(typed.AwayScore))
390
+ if kind == EntityPlayerDelivery {
391
+ return joinParts(lead, short, score, strings.Join(typed.Involvement, ","))
392
+ }
393
+ return joinParts(lead, short, score)
394
+ case map[string]any:
395
+ short := firstNonEmpty(valueString(typed, "shortText"), valueString(typed, "text"))
396
+ if short == "" {
397
+ short = joinParts("over "+valueString(typed, "overNumber"), "ball "+valueString(typed, "ballNumber"))
398
+ }
399
+ if strings.TrimSpace(short) == "/" || strings.TrimSpace(short) == "-" {
400
+ return ""
401
+ }
402
+ lead := overBallLabel(typed)
403
+ score := firstNonEmpty(scoreLabel(valueString(typed, "homeScore")), scoreLabel(valueString(typed, "awayScore")))
404
+ if kind == EntityPlayerDelivery {
405
+ return joinParts(lead, short, score, involvementLabel(typed))
406
+ }
407
+ return joinParts(lead, short, score)
408
+ default:
409
+ return ""
410
+ }
411
+ }
412
+
413
+ func overBallString(over, ball int) string {
414
+ if over <= 0 || ball <= 0 {
415
+ return ""
416
+ }
417
+ return fmt.Sprintf("%d.%d", over, ball)
418
+ }
419
+
420
+ func intString(value int) string {
421
+ if value <= 0 {
422
+ return ""
423
+ }
424
+ return fmt.Sprintf("%d", value)
425
+ }
426
+
427
+ func defaultNumeric(raw string) string {
428
+ raw = strings.TrimSpace(raw)
429
+ if raw == "" {
430
+ return "0"
431
+ }
432
+ return raw
433
+ }
434
+
435
+ func scoreLabel(raw string) string {
436
+ raw = strings.TrimSpace(raw)
437
+ if raw == "" {
438
+ return ""
439
+ }
440
+ if strings.Contains(raw, "/") {
441
+ return raw
442
+ }
443
+ return ""
444
+ }
445
+
298
446
  func sanitizeValue(value any, allFields bool) (any, error) {
299
447
  blob, err := json.Marshal(value)
300
448
  if err != nil {
@@ -339,6 +487,25 @@ func toMap(value any, allFields bool) (map[string]any, error) {
339
487
  return mapped, nil
340
488
  }
341
489
 
490
+ func mapFromAny(value any) map[string]any {
491
+ if value == nil {
492
+ return nil
493
+ }
494
+ if mapped, ok := value.(map[string]any); ok {
495
+ return mapped
496
+ }
497
+
498
+ blob, err := json.Marshal(value)
499
+ if err != nil {
500
+ return nil
501
+ }
502
+ var mapped map[string]any
503
+ if err := json.Unmarshal(blob, &mapped); err != nil {
504
+ return nil
505
+ }
506
+ return mapped
507
+ }
508
+
342
509
  func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) string {
343
510
  switch kind {
344
511
  case EntityMatch:
@@ -380,6 +547,20 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
380
547
  return joinParts("situation", data)
381
548
  }
382
549
  return joinParts("situation", valueString(entity, "competitionId"))
550
+ case EntityMatchDuel:
551
+ duelLabel := strings.TrimSpace(fmt.Sprintf("%s vs %s",
552
+ firstNonEmpty(valueString(entity, "batterName"), valueString(entity, "batterId")),
553
+ firstNonEmpty(valueString(entity, "bowlerName"), valueString(entity, "bowlerId")),
554
+ ))
555
+ return joinParts(
556
+ duelLabel,
557
+ fmt.Sprintf("%s off %s", valueString(entity, "runs"), valueString(entity, "balls")),
558
+ )
559
+ case EntityMatchPhases:
560
+ return joinParts(
561
+ "match "+firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")),
562
+ fmt.Sprintf("%d innings", len(sliceValue(entity, "innings"))),
563
+ )
383
564
  case EntityCompetition:
384
565
  return joinParts(
385
566
  firstNonEmpty(valueString(entity, "shortDescription"), valueString(entity, "description"), valueString(entity, "id")),
@@ -431,7 +612,15 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
431
612
  name := firstNonEmpty(valueString(entity, "name"), valueString(entity, "shortName"), valueString(entity, "id"))
432
613
  return joinParts(name, bracket(valueString(entity, "homeAway")))
433
614
  case EntityTeamRoster:
434
- name := firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "playerId"), valueString(entity, "athleteId"))
615
+ name := strings.TrimSpace(valueString(entity, "displayName"))
616
+ if name == "" {
617
+ playerID := firstNonEmpty(valueString(entity, "playerId"), valueString(entity, "athleteId"))
618
+ if playerID != "" {
619
+ name = "Unknown player (" + playerID + ")"
620
+ } else {
621
+ name = "Unknown player"
622
+ }
623
+ }
435
624
  badges := []string{}
436
625
  if valueString(entity, "captain") == "true" {
437
626
  badges = append(badges, "captain")
@@ -439,7 +628,7 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
439
628
  if valueString(entity, "active") == "true" {
440
629
  badges = append(badges, "active")
441
630
  }
442
- return joinParts(name, strings.Join(badges, ", "))
631
+ return joinParts(name, firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "teamId")), strings.Join(badges, ", "))
443
632
  case EntityTeamScore:
444
633
  return joinParts(valueString(entity, "displayValue"), valueString(entity, "value"), bracket(valueString(entity, "source")))
445
634
  case EntityTeamLeaders:
@@ -470,16 +659,26 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
470
659
  case EntityStandingsGroup:
471
660
  return joinParts(valueString(entity, "id"), "season "+valueString(entity, "seasonId"))
472
661
  case EntityInnings:
662
+ if valueString(entity, "score") == "" &&
663
+ valueString(entity, "runs") == "" &&
664
+ valueString(entity, "wickets") == "" &&
665
+ valueString(entity, "target") == "" &&
666
+ valueString(entity, "isBatting") == "false" {
667
+ return ""
668
+ }
473
669
  score := valueString(entity, "score")
474
670
  if score == "" {
475
671
  score = joinParts(valueString(entity, "runs")+"/"+valueString(entity, "wickets"), valueString(entity, "overs")+" ov")
476
672
  }
477
- return joinParts(
673
+ parts := []string{
478
674
  firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "teamId")),
479
- "innings "+valueString(entity, "inningsNumber")+"/"+valueString(entity, "period"),
675
+ "innings " + valueString(entity, "inningsNumber") + "/" + valueString(entity, "period"),
480
676
  score,
481
- fmt.Sprintf("%d wickets", len(sliceValue(entity, "wicketTimeline"))),
482
- )
677
+ }
678
+ if wc := len(sliceValue(entity, "wicketTimeline")); wc > 0 {
679
+ parts = append(parts, fmt.Sprintf("%d wickets", wc))
680
+ }
681
+ return joinParts(parts...)
483
682
  case EntityDeliveryEvent:
484
683
  short := firstNonEmpty(valueString(entity, "shortText"), valueString(entity, "text"))
485
684
  if short == "" {
@@ -489,16 +688,32 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
489
688
  case EntityStatCategory:
490
689
  return joinParts(firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "name")), fmt.Sprintf("%d stats", len(sliceValue(entity, "stats"))))
491
690
  case EntityPartnership:
691
+ runsText := ""
692
+ if runs := valueString(entity, "runs"); runs != "" {
693
+ runsText = runs + " runs"
694
+ } else if valueString(entity, "overs") != "" {
695
+ runsText = "0 runs"
696
+ }
697
+ oversText := ""
698
+ if overs := valueString(entity, "overs"); overs != "" {
699
+ oversText = overs + " ov"
700
+ }
492
701
  return joinParts(
493
702
  firstNonEmpty(valueString(entity, "wicketName"), "partnership "+valueString(entity, "id")),
494
- valueString(entity, "runs")+" runs",
495
- valueString(entity, "overs")+" ov",
703
+ runsText,
704
+ oversText,
496
705
  "innings "+valueString(entity, "inningsId")+"/"+valueString(entity, "period"),
497
706
  )
498
707
  case EntityFallOfWicket:
708
+ scoreText := ""
709
+ if runs := valueString(entity, "runs"); runs != "" {
710
+ scoreText = runs + "/" + valueString(entity, "wicketNumber")
711
+ } else if valueString(entity, "wicketNumber") == "1" {
712
+ scoreText = "0/1"
713
+ }
499
714
  return joinParts(
500
715
  "wicket "+valueString(entity, "wicketNumber"),
501
- valueString(entity, "runs")+"/"+valueString(entity, "wicketNumber"),
716
+ scoreText,
502
717
  valueString(entity, "wicketOver")+" ov",
503
718
  "innings "+valueString(entity, "inningsId")+"/"+valueString(entity, "period"),
504
719
  )
@@ -609,6 +824,8 @@ func formatSingleEntity(entity map[string]any, kind EntityKind, opts RenderOptio
609
824
  order = []string{"matchId", "competitionId", "eventId", "leagueId", "battingCards", "bowlingCards", "partnershipCards"}
610
825
  case EntityMatchSituation:
611
826
  order = []string{"matchId", "competitionId", "eventId", "leagueId", "oddsRef", "data"}
827
+ case EntityMatchPhases:
828
+ order = []string{"matchId", "competitionId", "eventId", "leagueId", "fixture", "result", "innings"}
612
829
  case EntityStatCategory:
613
830
  order = []string{"name", "displayName", "abbreviation"}
614
831
  case EntityPartnership:
@@ -715,7 +932,7 @@ func formatInningsTimelines(entity map[string]any) []string {
715
932
  row := joinParts(
716
933
  "Over "+valueString(over, "number"),
717
934
  valueString(over, "runs")+" runs",
718
- valueString(over, "wicketCount")+" wkts",
935
+ wicketCountLabel(valueString(over, "wicketCount")),
719
936
  )
720
937
  if row != "" {
721
938
  lines = append(lines, " "+row)
@@ -754,6 +971,14 @@ func formatInningsTimelines(entity map[string]any) []string {
754
971
  return lines
755
972
  }
756
973
 
974
+ func wicketCountLabel(raw string) string {
975
+ raw = strings.TrimSpace(raw)
976
+ if raw == "" || raw == "0" {
977
+ return ""
978
+ }
979
+ return raw + " wkts"
980
+ }
981
+
757
982
  func formatPlayerProfile(entity map[string]any) []string {
758
983
  lines := make([]string, 0, 16)
759
984
  if name := firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "fullName"), valueString(entity, "name")); name != "" {
@@ -790,7 +1015,7 @@ func formatPlayerProfile(entity map[string]any) []string {
790
1015
  }
791
1016
 
792
1017
  func formatPlayerMatchView(entity map[string]any) []string {
793
- lines := make([]string, 0, 16)
1018
+ lines := make([]string, 0, 48)
794
1019
  if player := valueString(entity, "playerName"); player != "" {
795
1020
  lines = append(lines, "Player: "+player)
796
1021
  }
@@ -806,13 +1031,21 @@ func formatPlayerMatchView(entity map[string]any) []string {
806
1031
  }
807
1032
  }
808
1033
  if batting := sliceValue(entity, "batting"); len(batting) > 0 {
809
- lines = append(lines, fmt.Sprintf("Batting Categories: %d", len(batting)))
1034
+ lines = append(lines, "Batting")
1035
+ lines = append(lines, formatStatCategoryList(batting)...)
810
1036
  }
811
1037
  if bowling := sliceValue(entity, "bowling"); len(bowling) > 0 {
812
- lines = append(lines, fmt.Sprintf("Bowling Categories: %d", len(bowling)))
1038
+ lines = append(lines, "Bowling")
1039
+ lines = append(lines, formatStatCategoryList(bowling)...)
813
1040
  }
814
1041
  if fielding := sliceValue(entity, "fielding"); len(fielding) > 0 {
815
- lines = append(lines, fmt.Sprintf("Fielding Categories: %d", len(fielding)))
1042
+ lines = append(lines, "Fielding")
1043
+ lines = append(lines, formatStatCategoryList(fielding)...)
1044
+ }
1045
+ if len(sliceValue(entity, "batting")) == 0 &&
1046
+ len(sliceValue(entity, "bowling")) == 0 &&
1047
+ len(sliceValue(entity, "fielding")) == 0 {
1048
+ lines = append(lines, "No match statistics categories available.")
816
1049
  }
817
1050
  return lines
818
1051
  }
@@ -867,6 +1100,224 @@ func formatMatchView(entity map[string]any) []string {
867
1100
  return lines
868
1101
  }
869
1102
 
1103
+ func formatMatchSituation(entity map[string]any) []string {
1104
+ lines := make([]string, 0, 64)
1105
+ if matchID := firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")); matchID != "" {
1106
+ lines = append(lines, "Match: "+matchID)
1107
+ }
1108
+
1109
+ live, _ := entity["live"].(map[string]any)
1110
+ if live == nil {
1111
+ if oddsRef := valueString(entity, "oddsRef"); oddsRef != "" {
1112
+ lines = append(lines, "Odds Ref: "+oddsRef)
1113
+ }
1114
+ if data, ok := entity["data"].(map[string]any); ok && len(data) > 0 {
1115
+ lines = append(lines, "Data: "+printableValue(data))
1116
+ }
1117
+ if len(lines) <= 1 {
1118
+ lines = append(lines, "No situation data available for this match.")
1119
+ }
1120
+ return lines
1121
+ }
1122
+
1123
+ if fixture := valueString(live, "fixture"); fixture != "" {
1124
+ lines = append(lines, "Fixture: "+fixture)
1125
+ }
1126
+ if status := valueString(live, "status"); status != "" {
1127
+ lines = append(lines, "Status: "+status)
1128
+ }
1129
+ scoreLine := joinParts(valueString(live, "score"), valueString(live, "overs"))
1130
+ if scoreLine != "" {
1131
+ lines = append(lines, "Score: "+scoreLine)
1132
+ }
1133
+ teamsLine := joinParts(
1134
+ "Batting "+valueString(live, "battingTeam"),
1135
+ "Bowling "+valueString(live, "bowlingTeam"),
1136
+ )
1137
+ if teamsLine != "" {
1138
+ lines = append(lines, teamsLine)
1139
+ }
1140
+ if snapshotAt := valueString(live, "snapshotAt"); snapshotAt != "" {
1141
+ lines = append(lines, "Snapshot: "+snapshotAt)
1142
+ }
1143
+ if stale := valueString(live, "stale"); stale == "true" {
1144
+ lines = append(lines, "Stale: true")
1145
+ if reason := valueString(live, "staleReason"); reason != "" {
1146
+ lines = append(lines, "Stale Reason: "+reason)
1147
+ }
1148
+ }
1149
+
1150
+ batters := sliceValue(live, "batters")
1151
+ if len(batters) > 0 {
1152
+ lines = append(lines, "Batters")
1153
+ for i, raw := range batters {
1154
+ batter, ok := raw.(map[string]any)
1155
+ if !ok {
1156
+ continue
1157
+ }
1158
+ score := fmt.Sprintf("%s(%s)", defaultNumeric(valueString(batter, "runs")), defaultNumeric(valueString(batter, "balls")))
1159
+ boundaries := joinParts("4s "+defaultNumeric(valueString(batter, "fours")), "6s "+defaultNumeric(valueString(batter, "sixes")))
1160
+ row := joinParts(
1161
+ firstNonEmpty(valueString(batter, "playerName"), valueString(batter, "playerId")),
1162
+ score,
1163
+ "SR "+defaultNumeric(valueString(batter, "strikeRate")),
1164
+ boundaries,
1165
+ )
1166
+ if strings.EqualFold(valueString(batter, "onStrike"), "true") {
1167
+ row = joinParts(row, "*")
1168
+ }
1169
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, row))
1170
+ }
1171
+ }
1172
+
1173
+ bowlers := sliceValue(live, "bowlers")
1174
+ if len(bowlers) > 0 {
1175
+ lines = append(lines, "Bowlers")
1176
+ for i, raw := range bowlers {
1177
+ bowler, ok := raw.(map[string]any)
1178
+ if !ok {
1179
+ continue
1180
+ }
1181
+ figures := fmt.Sprintf("%s-%s-%s-%s",
1182
+ oversLabelFromFields(valueString(bowler, "overs"), valueString(bowler, "balls")),
1183
+ defaultNumeric(valueString(bowler, "maidens")),
1184
+ defaultNumeric(valueString(bowler, "conceded")),
1185
+ defaultNumeric(valueString(bowler, "wickets")),
1186
+ )
1187
+ row := joinParts(
1188
+ firstNonEmpty(valueString(bowler, "playerName"), valueString(bowler, "playerId")),
1189
+ figures,
1190
+ "Econ "+defaultNumeric(valueString(bowler, "economy")),
1191
+ )
1192
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, row))
1193
+ }
1194
+ }
1195
+
1196
+ balls := sliceValue(live, "recentBalls")
1197
+ if len(balls) == 0 {
1198
+ balls = sliceValue(live, "currentOverBalls")
1199
+ }
1200
+ if len(balls) > 0 {
1201
+ lines = append(lines, "Recent Balls")
1202
+ for i, raw := range balls {
1203
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, summarizeDeliveryListItem(raw, EntityDeliveryEvent)))
1204
+ }
1205
+ }
1206
+ return lines
1207
+ }
1208
+
1209
+ func formatMatchDuel(entity map[string]any) []string {
1210
+ lines := make([]string, 0, 48)
1211
+ if matchID := valueString(entity, "matchId"); matchID != "" {
1212
+ lines = append(lines, "Match: "+matchID)
1213
+ }
1214
+ if fixture := valueString(entity, "fixture"); fixture != "" {
1215
+ lines = append(lines, "Fixture: "+fixture)
1216
+ }
1217
+ if score := valueString(entity, "score"); score != "" {
1218
+ lines = append(lines, "Score: "+score)
1219
+ }
1220
+ lines = append(lines, "Duel: "+firstNonEmpty(valueString(entity, "batterName"), valueString(entity, "batterId"))+" vs "+firstNonEmpty(valueString(entity, "bowlerName"), valueString(entity, "bowlerId")))
1221
+ summary := joinParts(
1222
+ fmt.Sprintf("%s off %s", defaultNumeric(valueString(entity, "runs")), defaultNumeric(valueString(entity, "balls"))),
1223
+ "SR "+defaultNumeric(valueString(entity, "strikeRate")),
1224
+ "dots "+defaultNumeric(valueString(entity, "dots")),
1225
+ "4s "+defaultNumeric(valueString(entity, "fours")),
1226
+ "6s "+defaultNumeric(valueString(entity, "sixes")),
1227
+ "wkts "+defaultNumeric(valueString(entity, "wickets")),
1228
+ )
1229
+ if strings.TrimSpace(summary) != "" {
1230
+ lines = append(lines, "Summary: "+summary)
1231
+ }
1232
+ if snapshot := valueString(entity, "snapshotAt"); snapshot != "" {
1233
+ lines = append(lines, "Snapshot: "+snapshot)
1234
+ }
1235
+ balls := sliceValue(entity, "recentBalls")
1236
+ if len(balls) > 0 {
1237
+ lines = append(lines, "Recent Duel Balls")
1238
+ for i, raw := range balls {
1239
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, summarizeDeliveryListItem(raw, EntityDeliveryEvent)))
1240
+ }
1241
+ }
1242
+ return lines
1243
+ }
1244
+
1245
+ func oversLabelFromFields(overs string, balls string) string {
1246
+ overs = strings.TrimSpace(overs)
1247
+ if overs != "" && overs != "0" {
1248
+ return overs
1249
+ }
1250
+ balls = strings.TrimSpace(balls)
1251
+ if balls == "" || balls == "0" {
1252
+ return "0.0"
1253
+ }
1254
+ b, err := strconv.Atoi(balls)
1255
+ if err != nil || b < 0 {
1256
+ return "0.0"
1257
+ }
1258
+ return fmt.Sprintf("%d.%d", b/6, b%6)
1259
+ }
1260
+
1261
+ func formatMatchPhases(entity map[string]any) []string {
1262
+ lines := make([]string, 0, 64)
1263
+ if matchID := firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")); matchID != "" {
1264
+ lines = append(lines, "Match: "+matchID)
1265
+ }
1266
+ if fixture := valueString(entity, "fixture"); fixture != "" {
1267
+ lines = append(lines, "Fixture: "+fixture)
1268
+ }
1269
+ if result := valueString(entity, "result"); result != "" {
1270
+ lines = append(lines, "Result: "+result)
1271
+ }
1272
+
1273
+ for _, raw := range sliceValue(entity, "innings") {
1274
+ inn, ok := raw.(map[string]any)
1275
+ if !ok {
1276
+ continue
1277
+ }
1278
+ lines = append(lines, "")
1279
+ lines = append(lines, joinParts(
1280
+ firstNonEmpty(valueString(inn, "teamName"), valueString(inn, "teamId")),
1281
+ "innings "+valueString(inn, "inningsNumber")+"/"+valueString(inn, "period"),
1282
+ valueString(inn, "score"),
1283
+ ))
1284
+
1285
+ lines = append(lines, " Phases")
1286
+ for _, key := range []string{"powerplay", "middle", "death"} {
1287
+ phaseMap, ok := inn[key].(map[string]any)
1288
+ if !ok {
1289
+ continue
1290
+ }
1291
+ name := firstNonEmpty(valueString(phaseMap, "name"), strings.Title(key))
1292
+ runs := defaultNumeric(valueString(phaseMap, "runs"))
1293
+ wickets := defaultNumeric(valueString(phaseMap, "wickets"))
1294
+ overs := defaultNumeric(valueString(phaseMap, "overs"))
1295
+ runRate := defaultNumeric(valueString(phaseMap, "runRate"))
1296
+ phaseLine := joinParts(
1297
+ name,
1298
+ "runs "+runs,
1299
+ "wkts "+wickets,
1300
+ "ov "+overs,
1301
+ "rr "+runRate,
1302
+ )
1303
+ lines = append(lines, " - "+phaseLine)
1304
+ }
1305
+
1306
+ bestOver := valueString(inn, "bestScoringOver")
1307
+ bestRuns := valueString(inn, "bestScoringOverRuns")
1308
+ if bestOver != "" && bestRuns != "" {
1309
+ lines = append(lines, " Best Over: over "+bestOver+" ("+bestRuns+" runs)")
1310
+ }
1311
+ collapseOver := valueString(inn, "collapseOver")
1312
+ collapseWickets := valueString(inn, "collapseWickets")
1313
+ if collapseOver != "" && collapseWickets != "" && collapseWickets != "0" {
1314
+ lines = append(lines, " Pressure Over: over "+collapseOver+" ("+collapseWickets+" wickets)")
1315
+ }
1316
+ }
1317
+
1318
+ return lines
1319
+ }
1320
+
870
1321
  func formatPlayerStatistics(entity map[string]any) []string {
871
1322
  lines := make([]string, 0, 64)
872
1323
 
@@ -1081,6 +1532,149 @@ func formatTeamLeaders(entity map[string]any) []string {
1081
1532
  return lines
1082
1533
  }
1083
1534
 
1535
+ func formatStatCategoryList(items []any) []string {
1536
+ lines := make([]string, 0, len(items)*4)
1537
+ for i, rawCategory := range items {
1538
+ category := mapFromAny(rawCategory)
1539
+ if category == nil {
1540
+ continue
1541
+ }
1542
+
1543
+ categoryName := firstNonEmpty(valueString(category, "displayName"), valueString(category, "name"), "Category")
1544
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, categoryName))
1545
+
1546
+ stats := sliceValue(category, "stats")
1547
+ if len(stats) == 0 {
1548
+ continue
1549
+ }
1550
+
1551
+ limit := len(stats)
1552
+ if limit > 16 {
1553
+ limit = 16
1554
+ }
1555
+ for j := 0; j < limit; j++ {
1556
+ statMap, ok := stats[j].(map[string]any)
1557
+ if !ok {
1558
+ continue
1559
+ }
1560
+ label := firstNonEmpty(valueString(statMap, "displayName"), valueString(statMap, "name"), valueString(statMap, "abbreviation"))
1561
+ value := firstNonEmpty(valueString(statMap, "displayValue"), valueString(statMap, "value"))
1562
+ row := joinParts(label, value, bracket(valueString(statMap, "abbreviation")))
1563
+ if row == "" {
1564
+ continue
1565
+ }
1566
+ lines = append(lines, fmt.Sprintf(" - %s", row))
1567
+ }
1568
+ if len(stats) > limit {
1569
+ lines = append(lines, fmt.Sprintf(" - ... %d more", len(stats)-limit))
1570
+ }
1571
+ }
1572
+ return lines
1573
+ }
1574
+
1575
+ func formatStandingsGroupList(items []any) []string {
1576
+ lines := make([]string, 0, len(items)*8+1)
1577
+ lines = append(lines, fmt.Sprintf("Standings Groups (%d)", len(items)))
1578
+
1579
+ for i, rawGroup := range items {
1580
+ group := mapFromAny(rawGroup)
1581
+ if group == nil {
1582
+ continue
1583
+ }
1584
+
1585
+ groupID := firstNonEmpty(valueString(group, "groupId"), valueString(group, "id"), fmt.Sprintf("%d", i+1))
1586
+ seasonID := valueString(group, "seasonId")
1587
+ header := "Group " + groupID
1588
+ if seasonID != "" {
1589
+ header = joinParts(header, "Season "+seasonID)
1590
+ }
1591
+ lines = append(lines, header)
1592
+
1593
+ entries := sliceValue(group, "entries")
1594
+ if len(entries) == 0 {
1595
+ lines = append(lines, " No standings entries available.")
1596
+ continue
1597
+ }
1598
+
1599
+ for entryIndex, rawEntry := range entries {
1600
+ team := mapFromAny(rawEntry)
1601
+ if team == nil {
1602
+ continue
1603
+ }
1604
+
1605
+ rank := standingsStatValueFromTeam(team, "rank", "position")
1606
+ if rank == "" {
1607
+ rank = fmt.Sprintf("%d", entryIndex+1)
1608
+ }
1609
+ teamName := firstNonEmpty(valueString(team, "shortName"), valueString(team, "name"), valueString(team, "id"))
1610
+ played := standingsStatValueFromTeam(team, "matchesplayed", "played", "matches")
1611
+ won := standingsStatValueFromTeam(team, "wins", "won")
1612
+ lost := standingsStatValueFromTeam(team, "losses", "lost")
1613
+ points := standingsStatValueFromTeam(team, "matchpoints", "points", "pts")
1614
+ nrr := standingsStatValueFromTeam(team, "netrunrate", "nrr", "runrate")
1615
+
1616
+ row := joinParts(
1617
+ fmt.Sprintf("#%s %s", rank, teamName),
1618
+ nonEmptyLabel("P", played),
1619
+ nonEmptyLabel("W", won),
1620
+ nonEmptyLabel("L", lost),
1621
+ nonEmptyLabel("Pts", points),
1622
+ nonEmptyLabel("NRR", nrr),
1623
+ )
1624
+ if strings.TrimSpace(row) == "" {
1625
+ row = joinParts(fmt.Sprintf("#%s %s", rank, teamName), valueString(team, "scoreSummary"))
1626
+ }
1627
+ lines = append(lines, " "+row)
1628
+ }
1629
+ }
1630
+ return lines
1631
+ }
1632
+
1633
+ func standingsStatValueFromTeam(team map[string]any, names ...string) string {
1634
+ if len(names) == 0 || team == nil {
1635
+ return ""
1636
+ }
1637
+
1638
+ targets := map[string]struct{}{}
1639
+ for _, name := range names {
1640
+ key := normalizeStatName(name)
1641
+ if key != "" {
1642
+ targets[key] = struct{}{}
1643
+ }
1644
+ }
1645
+
1646
+ extensions, ok := team["extensions"].(map[string]any)
1647
+ if !ok || extensions == nil {
1648
+ return ""
1649
+ }
1650
+ records, ok := extensions["records"].([]any)
1651
+ if !ok || len(records) == 0 {
1652
+ return ""
1653
+ }
1654
+
1655
+ for _, rawRecord := range records {
1656
+ record := mapFromAny(rawRecord)
1657
+ if record == nil {
1658
+ continue
1659
+ }
1660
+ for _, rawStat := range sliceValue(record, "stats") {
1661
+ stat := mapFromAny(rawStat)
1662
+ if stat == nil {
1663
+ continue
1664
+ }
1665
+ nameKey := normalizeStatName(firstNonEmpty(valueString(stat, "name"), valueString(stat, "type")))
1666
+ if _, ok := targets[nameKey]; !ok {
1667
+ continue
1668
+ }
1669
+ value := firstNonEmpty(valueString(stat, "displayValue"), valueString(stat, "value"))
1670
+ if value != "" {
1671
+ return value
1672
+ }
1673
+ }
1674
+ }
1675
+ return ""
1676
+ }
1677
+
1084
1678
  func formatAnalysisView(entity map[string]any) []string {
1085
1679
  lines := make([]string, 0, 64)
1086
1680
  if command := valueString(entity, "command"); command != "" {
@@ -1350,9 +1944,9 @@ func overBallLabel(entity map[string]any) string {
1350
1944
  return ""
1351
1945
  }
1352
1946
  if ball == "" {
1353
- return "over " + over
1947
+ return over
1354
1948
  }
1355
- return "over " + over + "." + ball
1949
+ return over + "." + ball
1356
1950
  }
1357
1951
 
1358
1952
  func involvementLabel(entity map[string]any) string {