nba 0.1.1 → 0.2.0

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.
Files changed (321) hide show
  1. checksums.yaml +5 -5
  2. data/AGENTS.md +362 -0
  3. data/CHANGELOG.md +169 -0
  4. data/CLAUDE.md +1 -0
  5. data/LICENSE +21 -0
  6. data/README.md +501 -101
  7. data/bin/console +10 -0
  8. data/bin/setup +6 -0
  9. data/exe/nba +8 -0
  10. data/lib/nba/all_time_leader.rb +77 -0
  11. data/lib/nba/all_time_leaders.rb +185 -0
  12. data/lib/nba/assist_leader.rb +92 -0
  13. data/lib/nba/assist_leaders.rb +64 -0
  14. data/lib/nba/assist_tracker.rb +108 -0
  15. data/lib/nba/assist_tracker_entry.rb +206 -0
  16. data/lib/nba/award.rb +128 -0
  17. data/lib/nba/box_score.rb +2 -0
  18. data/lib/nba/box_score_advanced.rb +114 -0
  19. data/lib/nba/box_score_advanced_player_stat.rb +297 -0
  20. data/lib/nba/box_score_advanced_team_stat.rb +237 -0
  21. data/lib/nba/box_score_advanced_v3.rb +124 -0
  22. data/lib/nba/box_score_defensive_player_stat.rb +281 -0
  23. data/lib/nba/box_score_defensive_team_stat.rb +85 -0
  24. data/lib/nba/box_score_defensive_v2.rb +190 -0
  25. data/lib/nba/box_score_four_factors.rb +91 -0
  26. data/lib/nba/box_score_four_factors_player_stat.rb +185 -0
  27. data/lib/nba/box_score_four_factors_team_stat.rb +141 -0
  28. data/lib/nba/box_score_four_factors_v3.rb +133 -0
  29. data/lib/nba/box_score_hustle.rb +226 -0
  30. data/lib/nba/box_score_hustle_player_stat.rb +233 -0
  31. data/lib/nba/box_score_hustle_team_stat.rb +189 -0
  32. data/lib/nba/box_score_matchup_stat.rb +417 -0
  33. data/lib/nba/box_score_matchups_v3.rb +184 -0
  34. data/lib/nba/box_score_misc.rb +100 -0
  35. data/lib/nba/box_score_misc_player_stat.rb +217 -0
  36. data/lib/nba/box_score_misc_team_stat.rb +173 -0
  37. data/lib/nba/box_score_misc_v3.rb +163 -0
  38. data/lib/nba/box_score_player_stat.rb +273 -0
  39. data/lib/nba/box_score_player_track.rb +223 -0
  40. data/lib/nba/box_score_player_track_stat.rb +273 -0
  41. data/lib/nba/box_score_player_track_team_stat.rb +229 -0
  42. data/lib/nba/box_score_scoring.rb +103 -0
  43. data/lib/nba/box_score_scoring_player_stat.rb +241 -0
  44. data/lib/nba/box_score_scoring_team_stat.rb +197 -0
  45. data/lib/nba/box_score_scoring_v3.rb +170 -0
  46. data/lib/nba/box_score_similarity_score.rb +119 -0
  47. data/lib/nba/box_score_similarity_stat.rb +76 -0
  48. data/lib/nba/box_score_starter_bench_stat.rb +257 -0
  49. data/lib/nba/box_score_summary.rb +285 -0
  50. data/lib/nba/box_score_summary_v2.rb +202 -0
  51. data/lib/nba/box_score_summary_v3.rb +120 -0
  52. data/lib/nba/box_score_summary_v3_data.rb +419 -0
  53. data/lib/nba/box_score_team_stat.rb +229 -0
  54. data/lib/nba/box_score_traditional.rb +101 -0
  55. data/lib/nba/box_score_traditional_v3.rb +195 -0
  56. data/lib/nba/box_score_usage.rb +102 -0
  57. data/lib/nba/box_score_usage_player_stat.rb +265 -0
  58. data/lib/nba/box_score_usage_team_stat.rb +221 -0
  59. data/lib/nba/box_score_usage_v3.rb +169 -0
  60. data/lib/nba/box_score_v3_helpers.rb +144 -0
  61. data/lib/nba/career_stats.rb +217 -0
  62. data/lib/nba/cli/display/player_display.rb +98 -0
  63. data/lib/nba/cli/display.rb +178 -0
  64. data/lib/nba/cli/formatters/game_formatters.rb +86 -0
  65. data/lib/nba/cli/formatters/leaders_formatters.rb +26 -0
  66. data/lib/nba/cli/formatters/player_formatters.rb +52 -0
  67. data/lib/nba/cli/formatters/standings_formatters.rb +26 -0
  68. data/lib/nba/cli/formatters/team_formatters.rb +67 -0
  69. data/lib/nba/cli/formatters/time_formatters.rb +82 -0
  70. data/lib/nba/cli/formatters.rb +56 -0
  71. data/lib/nba/cli/helpers.rb +135 -0
  72. data/lib/nba/cli.rb +171 -20
  73. data/lib/nba/client.rb +35 -0
  74. data/lib/nba/collection.rb +89 -0
  75. data/lib/nba/college_player_stat.rb +200 -0
  76. data/lib/nba/common_player_info.rb +142 -0
  77. data/lib/nba/common_playoff_series.rb +90 -0
  78. data/lib/nba/common_team_years.rb +113 -0
  79. data/lib/nba/conference.rb +39 -0
  80. data/lib/nba/connection.rb +84 -0
  81. data/lib/nba/cume_stats_player.rb +358 -0
  82. data/lib/nba/cume_stats_player_game.rb +217 -0
  83. data/lib/nba/cume_stats_player_games.rb +99 -0
  84. data/lib/nba/cume_stats_player_games_entry.rb +25 -0
  85. data/lib/nba/cume_stats_player_total.rb +481 -0
  86. data/lib/nba/cume_stats_team.rb +349 -0
  87. data/lib/nba/cume_stats_team_games.rb +145 -0
  88. data/lib/nba/cume_stats_team_games_entry.rb +25 -0
  89. data/lib/nba/cume_stats_team_player.rb +485 -0
  90. data/lib/nba/cume_stats_team_total.rb +267 -0
  91. data/lib/nba/data.rb +73 -0
  92. data/lib/nba/defense_hub.rb +109 -0
  93. data/lib/nba/defense_hub_stat.rb +57 -0
  94. data/lib/nba/defensive_shot_stat.rb +102 -0
  95. data/lib/nba/division.rb +49 -0
  96. data/lib/nba/draft_board.rb +126 -0
  97. data/lib/nba/draft_board_pick.rb +173 -0
  98. data/lib/nba/draft_combine_anthro_measurement.rb +163 -0
  99. data/lib/nba/draft_combine_drill_result.rb +115 -0
  100. data/lib/nba/draft_combine_drill_results.rb +112 -0
  101. data/lib/nba/draft_combine_non_stationary_shooting.rb +268 -0
  102. data/lib/nba/draft_combine_non_stationary_shooting_result.rb +355 -0
  103. data/lib/nba/draft_combine_player_anthro.rb +133 -0
  104. data/lib/nba/draft_combine_spot_shooting.rb +243 -0
  105. data/lib/nba/draft_combine_spot_shooting_result.rb +419 -0
  106. data/lib/nba/draft_combine_stat.rb +211 -0
  107. data/lib/nba/draft_combine_stats.rb +160 -0
  108. data/lib/nba/draft_history.rb +142 -0
  109. data/lib/nba/draft_pick.rb +154 -0
  110. data/lib/nba/dunk_score_leader.rb +93 -0
  111. data/lib/nba/dunk_score_leaders.rb +77 -0
  112. data/lib/nba/estimated_metrics_stat.rb +152 -0
  113. data/lib/nba/fantasy_profile_stat.rb +142 -0
  114. data/lib/nba/fantasy_widget.rb +72 -0
  115. data/lib/nba/fantasy_widget_player.rb +98 -0
  116. data/lib/nba/found_game.rb +260 -0
  117. data/lib/nba/franchise.rb +136 -0
  118. data/lib/nba/franchise_history.rb +142 -0
  119. data/lib/nba/franchise_leader.rb +147 -0
  120. data/lib/nba/franchise_leaders.rb +162 -0
  121. data/lib/nba/franchise_player.rb +224 -0
  122. data/lib/nba/franchise_players.rb +147 -0
  123. data/lib/nba/game.rb +80 -64
  124. data/lib/nba/game_log.rb +349 -0
  125. data/lib/nba/game_rotation.rb +152 -0
  126. data/lib/nba/game_streak.rb +102 -0
  127. data/lib/nba/games.rb +46 -0
  128. data/lib/nba/home_page_leader.rb +99 -0
  129. data/lib/nba/home_page_leaders.rb +75 -0
  130. data/lib/nba/home_page_stat.rb +57 -0
  131. data/lib/nba/home_page_v2.rb +110 -0
  132. data/lib/nba/hustle_stats_box_score.rb +182 -0
  133. data/lib/nba/infographic_fan_duel_player.rb +139 -0
  134. data/lib/nba/infographic_fan_duel_player_stat.rb +311 -0
  135. data/lib/nba/ist_standing.rb +167 -0
  136. data/lib/nba/ist_standings.rb +81 -0
  137. data/lib/nba/leader.rb +103 -0
  138. data/lib/nba/leaders.rb +110 -0
  139. data/lib/nba/leaders_tile.rb +57 -0
  140. data/lib/nba/leaders_tiles.rb +90 -0
  141. data/lib/nba/league.rb +37 -0
  142. data/lib/nba/league_dash_lineup_stat.rb +270 -0
  143. data/lib/nba/league_dash_lineups.rb +177 -0
  144. data/lib/nba/league_dash_opp_pt_shot.rb +150 -0
  145. data/lib/nba/league_dash_player_bio_stat.rb +217 -0
  146. data/lib/nba/league_dash_player_bio_stats.rb +164 -0
  147. data/lib/nba/league_dash_player_clutch.rb +212 -0
  148. data/lib/nba/league_dash_player_clutch_stat.rb +271 -0
  149. data/lib/nba/league_dash_player_pt_shot.rb +152 -0
  150. data/lib/nba/league_dash_player_pt_shot_stat.rb +193 -0
  151. data/lib/nba/league_dash_player_shot_location_stat.rb +265 -0
  152. data/lib/nba/league_dash_player_shot_locations.rb +210 -0
  153. data/lib/nba/league_dash_player_stat.rb +306 -0
  154. data/lib/nba/league_dash_player_stats.rb +176 -0
  155. data/lib/nba/league_dash_pt_defend.rb +160 -0
  156. data/lib/nba/league_dash_pt_defend_stat.rb +145 -0
  157. data/lib/nba/league_dash_pt_stats.rb +152 -0
  158. data/lib/nba/league_dash_pt_stats_stat.rb +169 -0
  159. data/lib/nba/league_dash_pt_team_defend.rb +158 -0
  160. data/lib/nba/league_dash_pt_team_defend_stat.rb +110 -0
  161. data/lib/nba/league_dash_team_clutch.rb +211 -0
  162. data/lib/nba/league_dash_team_clutch_stat.rb +237 -0
  163. data/lib/nba/league_dash_team_pt_shot.rb +150 -0
  164. data/lib/nba/league_dash_team_pt_shot_stat.rb +166 -0
  165. data/lib/nba/league_dash_team_shot_location_stat.rb +230 -0
  166. data/lib/nba/league_dash_team_shot_locations.rb +208 -0
  167. data/lib/nba/league_dash_team_stat.rb +275 -0
  168. data/lib/nba/league_dash_team_stats.rb +172 -0
  169. data/lib/nba/league_game_finder.rb +170 -0
  170. data/lib/nba/league_game_log.rb +224 -0
  171. data/lib/nba/league_hustle_stats_player.rb +161 -0
  172. data/lib/nba/league_hustle_stats_player_stat.rb +253 -0
  173. data/lib/nba/league_hustle_stats_team.rb +157 -0
  174. data/lib/nba/league_hustle_stats_team_stat.rb +179 -0
  175. data/lib/nba/league_lineup_viz.rb +184 -0
  176. data/lib/nba/league_lineup_viz_stat.rb +214 -0
  177. data/lib/nba/league_player_on_details.rb +175 -0
  178. data/lib/nba/league_player_on_details_stat.rb +313 -0
  179. data/lib/nba/league_season_matchup_stat.rb +241 -0
  180. data/lib/nba/league_season_matchups.rb +181 -0
  181. data/lib/nba/league_standing.rb +284 -0
  182. data/lib/nba/league_standings.rb +159 -0
  183. data/lib/nba/league_wide_shot_stat.rb +62 -0
  184. data/lib/nba/live_action.rb +240 -0
  185. data/lib/nba/live_box_score.rb +143 -0
  186. data/lib/nba/live_connection.rb +84 -0
  187. data/lib/nba/live_game.rb +230 -0
  188. data/lib/nba/live_play_by_play.rb +120 -0
  189. data/lib/nba/live_player_stat.rb +276 -0
  190. data/lib/nba/live_scoreboard.rb +102 -0
  191. data/lib/nba/matchup_rollup.rb +98 -0
  192. data/lib/nba/matchups_rollup.rb +81 -0
  193. data/lib/nba/pass_stat.rb +209 -0
  194. data/lib/nba/play.rb +258 -0
  195. data/lib/nba/play_by_play.rb +85 -0
  196. data/lib/nba/play_by_play_v3.rb +91 -0
  197. data/lib/nba/play_type_stat.rb +206 -0
  198. data/lib/nba/player.rb +242 -24
  199. data/lib/nba/player_awards.rb +110 -0
  200. data/lib/nba/player_career_by_college.rb +86 -0
  201. data/lib/nba/player_career_by_college_rollup.rb +143 -0
  202. data/lib/nba/player_career_stats.rb +77 -0
  203. data/lib/nba/player_compare.rb +156 -0
  204. data/lib/nba/player_comparison_stat.rb +242 -0
  205. data/lib/nba/player_dash_pt_pass.rb +164 -0
  206. data/lib/nba/player_dash_pt_reb.rb +235 -0
  207. data/lib/nba/player_dash_pt_shot_defend.rb +119 -0
  208. data/lib/nba/player_dash_pt_shots.rb +279 -0
  209. data/lib/nba/player_dashboard.rb +259 -0
  210. data/lib/nba/player_dashboard_stat.rb +248 -0
  211. data/lib/nba/player_estimated_metrics.rb +84 -0
  212. data/lib/nba/player_fantasy_profile_bar_graph.rb +147 -0
  213. data/lib/nba/player_game_log.rb +72 -0
  214. data/lib/nba/player_game_logs.rb +117 -0
  215. data/lib/nba/player_game_streak_finder.rb +108 -0
  216. data/lib/nba/player_index.rb +135 -0
  217. data/lib/nba/player_index_entry.rb +266 -0
  218. data/lib/nba/player_info.rb +225 -0
  219. data/lib/nba/player_next_n_games.rb +64 -0
  220. data/lib/nba/player_profile_v2.rb +169 -0
  221. data/lib/nba/player_vs_player.rb +153 -0
  222. data/lib/nba/players.rb +107 -0
  223. data/lib/nba/playoff_matchup.rb +84 -0
  224. data/lib/nba/playoff_picture.rb +98 -0
  225. data/lib/nba/playoff_series.rb +76 -0
  226. data/lib/nba/position.rb +48 -0
  227. data/lib/nba/rebound_stat.rb +189 -0
  228. data/lib/nba/response_parser.rb +116 -0
  229. data/lib/nba/roster.rb +74 -0
  230. data/lib/nba/rotation_entry.rb +154 -0
  231. data/lib/nba/schedule.rb +183 -0
  232. data/lib/nba/schedule_international.rb +182 -0
  233. data/lib/nba/scheduled_game.rb +240 -0
  234. data/lib/nba/scoreboard.rb +183 -0
  235. data/lib/nba/scoreboard_v3.rb +104 -0
  236. data/lib/nba/shot.rb +208 -0
  237. data/lib/nba/shot_chart.rb +75 -0
  238. data/lib/nba/shot_chart_league_wide.rb +102 -0
  239. data/lib/nba/shot_chart_lineup_detail.rb +109 -0
  240. data/lib/nba/shot_stat.rb +174 -0
  241. data/lib/nba/standing.rb +129 -0
  242. data/lib/nba/standings.rb +75 -0
  243. data/lib/nba/static.rb +107 -0
  244. data/lib/nba/synergy_play_types.rb +211 -0
  245. data/lib/nba/team.rb +203 -127
  246. data/lib/nba/team_and_players_vs_players.rb +227 -0
  247. data/lib/nba/team_and_players_vs_players_stat.rb +155 -0
  248. data/lib/nba/team_dash_pt_pass.rb +157 -0
  249. data/lib/nba/team_dash_pt_reb.rb +216 -0
  250. data/lib/nba/team_dash_pt_shots.rb +244 -0
  251. data/lib/nba/team_dashboard.rb +275 -0
  252. data/lib/nba/team_dashboard_stat.rb +248 -0
  253. data/lib/nba/team_detail.rb +117 -0
  254. data/lib/nba/team_details.rb +173 -0
  255. data/lib/nba/team_estimated_metrics.rb +91 -0
  256. data/lib/nba/team_estimated_metrics_stat.rb +146 -0
  257. data/lib/nba/team_game_log.rb +143 -0
  258. data/lib/nba/team_game_log_entry.rb +246 -0
  259. data/lib/nba/team_game_log_stat.rb +275 -0
  260. data/lib/nba/team_game_logs.rb +163 -0
  261. data/lib/nba/team_game_streak.rb +111 -0
  262. data/lib/nba/team_game_streak_finder.rb +109 -0
  263. data/lib/nba/team_historical_leader.rb +207 -0
  264. data/lib/nba/team_historical_leaders.rb +98 -0
  265. data/lib/nba/team_historical_record.rb +139 -0
  266. data/lib/nba/team_info.rb +150 -0
  267. data/lib/nba/team_info_common.rb +177 -0
  268. data/lib/nba/team_on_off_overall_stat.rb +477 -0
  269. data/lib/nba/team_on_off_player_stat.rb +523 -0
  270. data/lib/nba/team_on_off_player_summary.rb +135 -0
  271. data/lib/nba/team_pass_stat.rb +183 -0
  272. data/lib/nba/team_player_dashboard.rb +212 -0
  273. data/lib/nba/team_player_on_off_details.rb +218 -0
  274. data/lib/nba/team_player_on_off_summary.rb +214 -0
  275. data/lib/nba/team_player_stat.rb +275 -0
  276. data/lib/nba/team_rebound_stat.rb +189 -0
  277. data/lib/nba/team_season_rank.rb +110 -0
  278. data/lib/nba/team_shot_stat.rb +173 -0
  279. data/lib/nba/team_vs_player.rb +151 -0
  280. data/lib/nba/team_vs_player_stat.rb +157 -0
  281. data/lib/nba/team_year.rb +55 -0
  282. data/lib/nba/team_year_by_year_stats.rb +152 -0
  283. data/lib/nba/team_year_stat.rb +282 -0
  284. data/lib/nba/teams.rb +33 -0
  285. data/lib/nba/upcoming_game.rb +115 -0
  286. data/lib/nba/utils.rb +94 -0
  287. data/lib/nba/version.rb +5 -2
  288. data/lib/nba/video_detail.rb +103 -0
  289. data/lib/nba/video_details.rb +118 -0
  290. data/lib/nba/video_details_asset.rb +115 -0
  291. data/lib/nba/video_details_asset_entry.rb +91 -0
  292. data/lib/nba/video_event.rb +83 -0
  293. data/lib/nba/video_event_asset.rb +91 -0
  294. data/lib/nba/video_events.rb +106 -0
  295. data/lib/nba/video_events_asset.rb +107 -0
  296. data/lib/nba/video_status.rb +129 -0
  297. data/lib/nba/video_status_entry.rb +161 -0
  298. data/lib/nba/vs_player_stat.rb +156 -0
  299. data/lib/nba/win_probability.rb +117 -0
  300. data/lib/nba/win_probability_point.rb +140 -0
  301. data/lib/nba.rb +249 -5
  302. data/sig/equalizer.rbs +3 -0
  303. data/sig/nba.rbs +7297 -0
  304. data/sig/shale.rbs +24 -0
  305. data/sig/thor.rbs +19 -0
  306. metadata +324 -95
  307. data/.gitignore +0 -18
  308. data/.travis.yml +0 -22
  309. data/Gemfile +0 -23
  310. data/LICENSE.md +0 -22
  311. data/Rakefile +0 -18
  312. data/bin/nba +0 -7
  313. data/cache/teams.json +0 -16529
  314. data/lib/faraday_middleware/scrape_game.rb +0 -41
  315. data/lib/nba/request.rb +0 -37
  316. data/nba.gemspec +0 -28
  317. data/spec/fixtures/games.html +0 -785
  318. data/spec/fixtures/teams.json +0 -16529
  319. data/spec/game_spec.rb +0 -40
  320. data/spec/spec_helper.rb +0 -25
  321. data/spec/team_spec.rb +0 -93
@@ -0,0 +1,67 @@
1
+ module NBA
2
+ class CLI < Thor
3
+ module Formatters
4
+ # Formatters for team-related output
5
+ module TeamFormatters
6
+ # Eastern Conference team abbreviations
7
+ #
8
+ # @api private
9
+ # @return [Array<String>]
10
+ EAST = %w[ATL BOS BKN CHA CHI CLE DET IND MIA MIL NYK ORL PHI TOR WAS].freeze
11
+
12
+ # Division mappings by team abbreviation
13
+ #
14
+ # @api private
15
+ # @return [Hash]
16
+ DIVISIONS = {
17
+ %w[BOS BKN NYK PHI TOR] => "Atlantic", %w[CHI CLE DET IND MIL] => "Central",
18
+ %w[ATL CHA MIA ORL WAS] => "Southeast", %w[DEN MIN OKC POR UTA] => "Northwest",
19
+ %w[GSW LAC LAL PHX SAC] => "Pacific", %w[DAL HOU MEM NOP SAS] => "Southwest"
20
+ }.freeze
21
+
22
+ # Returns the team nickname for display
23
+ #
24
+ # @api private
25
+ # @return [String]
26
+ def team_nickname(team)
27
+ return "TBD" unless team
28
+
29
+ team.nickname || team.full_name&.split&.last || "TBD"
30
+ end
31
+
32
+ # Returns the conference name for a team
33
+ #
34
+ # @api private
35
+ # @return [String]
36
+ def conference_name(detail)
37
+ EAST.include?(detail.abbreviation.to_s) ? "Eastern Conference" : "Western Conference"
38
+ end
39
+
40
+ # Returns the division name for a team
41
+ #
42
+ # @api private
43
+ # @return [String, nil]
44
+ def division_name(detail)
45
+ division_for_team(detail.abbreviation.to_s)
46
+ end
47
+
48
+ # Looks up the division for a team abbreviation
49
+ #
50
+ # @api private
51
+ # @return [String, nil]
52
+ def division_for_team(abbr)
53
+ division = DIVISIONS.find { |teams, _| teams.include?(abbr) }&.last
54
+ "#{division} Division" if division
55
+ end
56
+
57
+ # Returns whether the stat represents a championship year
58
+ #
59
+ # @api private
60
+ # @return [Boolean]
61
+ def championship_year?(stat)
62
+ stat.nba_finals_appearance.eql?("LEAGUE CHAMPION")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,82 @@
1
+ module NBA
2
+ class CLI < Thor
3
+ module Formatters
4
+ # Formatters for time-related output
5
+ module TimeFormatters
6
+ # Pattern to match Eastern time format (e.g., "7:30 pm ET")
7
+ # @return [Regexp] the time pattern
8
+ ET_TIME_PATTERN = /\A(\d{1,2}):(\d{2})\s*(am|pm)\s*ET\z/i
9
+
10
+ # Converts Eastern time to local time zone
11
+ #
12
+ # @api private
13
+ # @param status [String] the status string (may contain ET time)
14
+ # @return [String] the status with time converted to local zone
15
+ def convert_et_to_local(status)
16
+ match = ET_TIME_PATTERN.match(status)
17
+ return status unless match
18
+
19
+ format_local_time(parse_et_time(match))
20
+ end
21
+
22
+ # Parses Eastern time components into a Time object
23
+ #
24
+ # @api private
25
+ # @param match [MatchData] the regex match with hour, minute, am/pm
26
+ # @return [Time] the time in Eastern timezone
27
+ def parse_et_time(match)
28
+ hour = Integer(match[1] || 0)
29
+ minute = Integer(match[2] || 0)
30
+ period = (match[3] || "am").downcase
31
+
32
+ hour = convert_to_24h(hour, period)
33
+ build_et_time(hour, minute)
34
+ end
35
+
36
+ # Converts 12-hour format to 24-hour format
37
+ #
38
+ # @api private
39
+ # @param hour [Integer] the hour in 12-hour format
40
+ # @param period [String] "am" or "pm"
41
+ # @return [Integer] the hour in 24-hour format
42
+ def convert_to_24h(hour, period)
43
+ if period.eql?("am")
44
+ hour.eql?(12) ? 0 : hour
45
+ else
46
+ hour.eql?(12) ? 12 : hour + 12
47
+ end
48
+ end
49
+
50
+ # Builds a Time object in Eastern timezone
51
+ #
52
+ # @api private
53
+ # @param hour [Integer] the hour in 24-hour format
54
+ # @param minute [Integer] the minute
55
+ # @return [Time] the time in Eastern timezone
56
+ def build_et_time(hour, minute)
57
+ today = Date.today
58
+ Time.new(today.year, today.month, today.day, hour, minute, nil, "-05:00")
59
+ end
60
+
61
+ # Formats a time in the local timezone
62
+ #
63
+ # @api private
64
+ # @param et_time [Time] the time in Eastern timezone
65
+ # @return [String] formatted local time string
66
+ def format_local_time(et_time)
67
+ local_time = et_time.localtime
68
+ zone_abbr = local_time_zone_abbr
69
+ local_time.strftime("%-I:%M %p #{zone_abbr}")
70
+ end
71
+
72
+ # Returns the local timezone abbreviation
73
+ #
74
+ # @api private
75
+ # @return [String] the timezone abbreviation (e.g., "PST", "EST")
76
+ def local_time_zone_abbr
77
+ Time.now.zone || "ET"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,56 @@
1
+ require_relative "formatters/time_formatters"
2
+ require_relative "formatters/team_formatters"
3
+ require_relative "formatters/game_formatters"
4
+ require_relative "formatters/standings_formatters"
5
+ require_relative "formatters/leaders_formatters"
6
+ require_relative "formatters/player_formatters"
7
+
8
+ module NBA
9
+ class CLI < Thor
10
+ # Helper methods for formatting CLI output
11
+ module Formatters
12
+ include TimeFormatters
13
+ include TeamFormatters
14
+ include GameFormatters
15
+ include StandingsFormatters
16
+ include LeadersFormatters
17
+ include PlayerFormatters
18
+
19
+ # Standard label width for formatted output
20
+ #
21
+ # @api private
22
+ # @return [Integer]
23
+ LABEL_WIDTH = 16
24
+
25
+ # Returns the maximum string length from the given values
26
+ #
27
+ # @api private
28
+ # @return [Integer]
29
+ def max_length(values) = values.map { |v| v.to_s.length }.max
30
+
31
+ # Centers a value within the given width
32
+ #
33
+ # @api private
34
+ # @return [String]
35
+ def center(value, width) = value.to_s.center(width)
36
+
37
+ # Formats a label and value as a single line
38
+ #
39
+ # @api private
40
+ # @return [String]
41
+ def format_label(label, value) = "#{label}: #{value}"
42
+
43
+ # Formats a label with multiple items across lines
44
+ #
45
+ # @api private
46
+ # @return [String]
47
+ def format_multiline_label(label, items)
48
+ indent = " " * (label.length + 2)
49
+ lines = items.each_slice(2).map { |pair| pair.join(", ") }
50
+ first_line = "#{label}: #{lines.first}"
51
+ continuation = lines.drop(1).map { |line| "#{indent}#{line}" }
52
+ ([first_line] + continuation).join("\n")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,135 @@
1
+ module NBA
2
+ class CLI < Thor
3
+ # Helper methods for CLI date parsing and team lookup
4
+ module Helpers
5
+ # Eastern Time offset from UTC in seconds
6
+ # @return [Integer] offset value in seconds
7
+ ET_OFFSET_SECONDS = 5 * 60 * 60
8
+
9
+ # Mapping of conference abbreviations to full names
10
+ # @return [Hash<String, String>] conference mapping
11
+ CONFERENCE_MAP = {"E" => "East", "W" => "West", nil => "Invalid"}.freeze
12
+
13
+ # Parses a date string into a Date object
14
+ #
15
+ # @api private
16
+ # @param date_str [String, nil] the date string
17
+ # @return [Date] the parsed date
18
+ def parse_date(date_str)
19
+ return eastern_time_date if date_str.nil? || date_str.eql?("today")
20
+ return eastern_time_date - 1 if date_str.eql?("yesterday")
21
+ return eastern_time_date + 1 if date_str.eql?("tomorrow")
22
+
23
+ parse_date_string(date_str)
24
+ end
25
+
26
+ # Parses a YYYYMMDD date string
27
+ #
28
+ # @api private
29
+ # @param date_str [String] the date string in YYYYMMDD format
30
+ # @return [Date] the parsed date
31
+ # @raise [SystemExit] if the date string is invalid
32
+ def parse_date_string(date_str)
33
+ Date.strptime(date_str, "%Y%m%d")
34
+ rescue Date::Error
35
+ say("Invalid date '#{date_str}'. Use YYYYMMDD format, 'today', 'yesterday', or 'tomorrow'.")
36
+ raise SystemExit
37
+ end
38
+
39
+ # Returns the current date in Eastern Time
40
+ #
41
+ # @api private
42
+ # @return [Date] the current Eastern Time date
43
+ def eastern_time_date = (Time.now.utc - ET_OFFSET_SECONDS).to_date
44
+
45
+ # Finds a team by name or abbreviation
46
+ #
47
+ # @api private
48
+ # @param name [String] the team name or abbreviation
49
+ # @return [Team, nil] the matching team or nil
50
+ def find_team_by_name(name)
51
+ pattern = Regexp.new(name, Regexp::IGNORECASE)
52
+ Teams.all.find { |t| pattern.match?(t.full_name) || pattern.match?(t.abbreviation) }
53
+ end
54
+
55
+ # Filters teams by name or abbreviation pattern, or returns all teams
56
+ #
57
+ # @api private
58
+ # @param name [String, nil] the team name or abbreviation pattern to filter by
59
+ # @return [Collection, Array] the matching teams
60
+ def filter_teams(name)
61
+ return Teams.all unless name
62
+
63
+ pattern = Regexp.new(name, Regexp::IGNORECASE)
64
+ Teams.all.select { |team| pattern.match?(team.full_name) || pattern.match?(team.abbreviation) }
65
+ end
66
+
67
+ # Normalizes a conference input to full name
68
+ #
69
+ # @api private
70
+ # @param input [String] the conference input (e.g., "e", "E", "East", "w", "W", "West")
71
+ # @return [String] the normalized conference name
72
+ def normalize_conference(input)
73
+ CONFERENCE_MAP.fetch(input.upcase[0].to_s, input)
74
+ end
75
+
76
+ # Fetches standings based on options
77
+ #
78
+ # @api private
79
+ # @return [Collection] the standings collection
80
+ def fetch_standings
81
+ return fetch_conference_standings if options[:conference]
82
+
83
+ options[:season] ? Standings.all(season: options.fetch(:season)) : Standings.all
84
+ end
85
+
86
+ # Fetches conference-specific standings
87
+ #
88
+ # @api private
89
+ # @return [Collection] the conference standings
90
+ def fetch_conference_standings
91
+ conf = normalize_conference(options.fetch(:conference))
92
+ options[:season] ? Standings.conference(conf, season: options.fetch(:season)) : Standings.conference(conf)
93
+ end
94
+
95
+ # Fetches schedule for a team
96
+ #
97
+ # @api private
98
+ # @param team [Team] the team
99
+ # @return [Collection] the schedule
100
+ def fetch_team_schedule(team)
101
+ season = options[:season]
102
+ season ? Schedule.by_team(team: team, season: season) : Schedule.by_team(team: team)
103
+ end
104
+
105
+ # Fetches roster for a team
106
+ #
107
+ # @api private
108
+ # @param team [Team] the team
109
+ # @return [Collection] the roster
110
+ def fetch_team_roster(team)
111
+ season = options[:season]
112
+ season ? Roster.find(team: team, season: season) : Roster.find(team: team)
113
+ end
114
+
115
+ # Resolves a category name to a Leaders constant
116
+ #
117
+ # @api private
118
+ # @param category [String] the category name
119
+ # @param category_map [Hash] mapping of category names to constant names
120
+ # @return [String] the Leaders constant value
121
+ def resolve_leader_category(category, category_map)
122
+ category_map[category.upcase] || Leaders::PTS
123
+ end
124
+
125
+ # Fetches games for a date, using live data for today
126
+ #
127
+ # @api private
128
+ # @param date [Date] the date
129
+ # @return [Collection] the games collection
130
+ def fetch_games(date)
131
+ date.eql?(eastern_time_date) ? LiveScoreboard.today : Scoreboard.games(date: date)
132
+ end
133
+ end
134
+ end
135
+ end
data/lib/nba/cli.rb CHANGED
@@ -1,35 +1,186 @@
1
- require 'thor'
1
+ require "thor"
2
+ require "date"
3
+ require_relative "cli/formatters"
4
+ require_relative "cli/display"
5
+ require_relative "cli/helpers"
2
6
 
3
7
  module NBA
8
+ # Command-line interface for the NBA gem
9
+ #
10
+ # @api public
4
11
  class CLI < Thor
5
- desc "games DATE", "Retrieve games' scoreboard of date"
6
- method_option :date, :aliases => '-d', :desc => "format: YYYYMMDD"
12
+ include Formatters
13
+ include Display
14
+ include Helpers
15
+
16
+ # Mapping of category names and abbreviations to Leaders constants
17
+ # @return [Hash<String, String>] category mapping
18
+ CATEGORY_MAP = {
19
+ "PTS" => "PTS", "POINTS" => "PTS",
20
+ "REB" => "REB", "REBOUNDS" => "REB",
21
+ "AST" => "AST", "ASSISTS" => "AST",
22
+ "STL" => "STL", "STEALS" => "STL",
23
+ "BLK" => "BLK", "BLOCKS" => "BLK",
24
+ "FG_PCT" => "FG_PCT", "FG3_PCT" => "FG3_PCT", "FT_PCT" => "FT_PCT"
25
+ }.freeze
26
+
27
+ class_option :version, type: :boolean, aliases: "-v", desc: "Print version and exit"
28
+
29
+ remove_command :tree
30
+
31
+ # Returns whether Thor should exit on failure
32
+ #
33
+ # @api public
34
+ # @example
35
+ # NBA::CLI.exit_on_failure? #=> true
36
+ # @return [Boolean] true if CLI should exit on failure
37
+ def self.exit_on_failure?
38
+ true
39
+ end
40
+
41
+ desc "games", "Retrieve games' scoreboard for a date"
42
+ method_option :date, type: :string, aliases: "-d", desc: "Date (YYYYMMDD, 'today', or 'yesterday')"
43
+ # Retrieves and displays games for a specified date
44
+ #
45
+ # @api public
46
+ # @example
47
+ # cli = NBA::CLI.new
48
+ # cli.games
49
+ # @return [void]
7
50
  def games
8
- if options[:date].nil? || options[:date] == 'today'
9
- game_date_in_et = (Time.now.utc - 5 * 60 * 60).strftime("%Y%m%d")
10
- elsif options[:date] == 'yesterday'
11
- game_date_in_et = (Time.now.utc - (5 + 24) * 60 * 60).strftime("%Y%m%d")
51
+ date = parse_date(options[:date])
52
+ games_list = fetch_games(date)
53
+ games_list.empty? ? say("No games found for #{date}") : display_games(games_list)
54
+ end
55
+
56
+ desc "teams [NAME]", "List all teams or search by name"
57
+ method_option :roster, type: :boolean, aliases: "-r", default: false, desc: "Include roster"
58
+ # Lists all teams or searches for teams by name
59
+ #
60
+ # @api public
61
+ # @example
62
+ # cli = NBA::CLI.new
63
+ # cli.teams("GSW")
64
+ # @param name [String, nil] the team name or abbreviation to search for
65
+ # @return [void]
66
+ def teams(name = nil)
67
+ matching_teams = filter_teams(name)
68
+ if matching_teams.empty?
69
+ say("No team found with name '#{name}'")
12
70
  else
13
- game_date_in_et = options[:date]
71
+ display_teams(matching_teams, options.fetch(:roster), detailed: name)
14
72
  end
73
+ end
15
74
 
16
- Game.all(game_date_in_et)
75
+ desc "player NAME", "Search for a player by name"
76
+ # Searches for players by name
77
+ #
78
+ # @api public
79
+ # @example
80
+ # cli = NBA::CLI.new
81
+ # cli.player("LeBron")
82
+ # @param name [String] the player name to search for
83
+ # @return [void]
84
+ def player(name)
85
+ pattern = Regexp.new(name, Regexp::IGNORECASE)
86
+ matching = Players.all.select { |p| pattern.match?(p.full_name) }
87
+ if matching.empty?
88
+ say("No player found with name '#{name}'")
89
+ elsif matching.one?
90
+ matching.each { |p| display_player(p) }
91
+ else
92
+ display_players(matching)
93
+ end
17
94
  end
18
95
 
19
- desc "teams", "Retrieve teams' info, with players' info if `-p` arg is passed"
20
- method_option :name, :type => :string, :aliases => "-n", :required => true
21
- method_option :players, :type => :boolean, :aliases => "-p", :default => false
22
- def teams
23
- team_results = Team.all.select { |team| team.name =~ %r|#{options[:name]}|i }
96
+ desc "standings", "Display current league standings"
97
+ method_option :conference, type: :string, aliases: "-c", desc: "Filter by conference (East/West)"
98
+ method_option :season, type: :numeric, aliases: "-s", desc: "Season year (e.g., 2024)"
99
+ # Displays current league standings
100
+ #
101
+ # @api public
102
+ # @example
103
+ # cli = NBA::CLI.new
104
+ # cli.standings
105
+ # @return [void]
106
+ def standings
107
+ display_standings(fetch_standings)
108
+ end
24
109
 
25
- if team_results.count == 0
26
- puts "No team founded with name #{options[:name]}"
110
+ desc "leaders [CATEGORY]", "Display league leaders for a statistical category"
111
+ method_option :season, type: :numeric, aliases: "-s", desc: "Season year (e.g., 2024)"
112
+ method_option :limit, type: :numeric, aliases: "-l", default: 10, desc: "Number of leaders"
113
+ # Displays league leaders for a statistical category
114
+ #
115
+ # @api public
116
+ # @example
117
+ # cli = NBA::CLI.new
118
+ # cli.leaders("PTS")
119
+ # @param category_name [String] the stat category (PTS, REB, AST, STL, BLK, FG_PCT, FG3_PCT, FT_PCT)
120
+ # @return [void]
121
+ def leaders(category_name = "PTS")
122
+ category = resolve_leader_category(category_name, CATEGORY_MAP)
123
+ leaders_list = if options[:season]
124
+ Leaders.find(category: category, limit: options.fetch(:limit),
125
+ season: options.fetch(:season))
27
126
  else
28
- team_results.each do |team|
29
- team.pretty_print
30
- puts "-" * 42
31
- end
127
+ Leaders.find(category: category, limit: options.fetch(:limit))
32
128
  end
129
+ display_leaders(leaders_list, category_name)
33
130
  end
131
+
132
+ desc "schedule TEAM", "Display schedule for a team"
133
+ method_option :season, type: :numeric, aliases: "-s", desc: "Season year (e.g., 2024)"
134
+ # Displays schedule for a team
135
+ #
136
+ # @api public
137
+ # @example
138
+ # cli = NBA::CLI.new
139
+ # cli.schedule("Lakers")
140
+ # @param team_name [String] the team name or abbreviation
141
+ # @return [void]
142
+ def schedule(team_name)
143
+ team = find_team_by_name(team_name)
144
+ if team
145
+ schedule_list = fetch_team_schedule(team)
146
+ display_schedule(schedule_list, team)
147
+ else
148
+ say("No team found with name '#{team_name}'")
149
+ end
150
+ end
151
+
152
+ desc "roster TEAM", "Display roster for a team"
153
+ method_option :season, type: :numeric, aliases: "-s", desc: "Season year (e.g., 2024)"
154
+ # Displays roster for a team
155
+ #
156
+ # @api public
157
+ # @example
158
+ # cli = NBA::CLI.new
159
+ # cli.roster("Lakers")
160
+ # @param team_name [String] the team name or abbreviation
161
+ # @return [void]
162
+ def roster(team_name)
163
+ team = find_team_by_name(team_name)
164
+ if team
165
+ roster_list = fetch_team_roster(team)
166
+ display_roster(roster_list, team)
167
+ else
168
+ say("No team found with name '#{team_name}'")
169
+ end
170
+ end
171
+
172
+ desc "version", "Display version information"
173
+ # Displays version information
174
+ #
175
+ # @api public
176
+ # @example
177
+ # cli = NBA::CLI.new
178
+ # cli.version
179
+ # @return [void]
180
+ def version
181
+ say("nba #{VERSION}")
182
+ end
183
+
184
+ map %w[-v --version] => :version
34
185
  end
35
186
  end
data/lib/nba/client.rb ADDED
@@ -0,0 +1,35 @@
1
+ require_relative "connection"
2
+
3
+ module NBA
4
+ # API client for making requests to the NBA Stats API
5
+ class Client
6
+ # Makes a GET request to the specified path
7
+ #
8
+ # @api public
9
+ # @example
10
+ # client.get("commonplayerinfo?PlayerID=2544")
11
+ # @param path [String] the API path to request
12
+ # @return [String] the response body
13
+ def get(path) = connection.get(path)
14
+
15
+ # Returns the connection used for HTTP requests
16
+ #
17
+ # @api private
18
+ # @return [Connection] the connection instance
19
+ attr_reader :connection
20
+
21
+ # Initializes a new Client object
22
+ #
23
+ # @api public
24
+ # @example
25
+ # client = NBA::Client.new
26
+ # @param connection [Connection] the connection to use for requests
27
+ # @return [NBA::Client] a new client instance
28
+ def initialize(connection: Connection.new)
29
+ @connection = connection
30
+ end
31
+ end
32
+
33
+ # Default client instance
34
+ CLIENT = Client.new
35
+ end
@@ -0,0 +1,89 @@
1
+ require "equalizer"
2
+ require "forwardable"
3
+
4
+ module NBA
5
+ # Represents a collection of objects
6
+ class Collection
7
+ extend Forwardable
8
+ include Equalizer.new(:elements)
9
+ include Enumerable
10
+
11
+ # @!method size
12
+ # Returns the number of elements in the collection
13
+ # @api public
14
+ # @example
15
+ # collection.size #=> 3
16
+ # @return [Integer] the number of elements
17
+
18
+ # @!method empty?
19
+ # Returns true if the collection has no elements
20
+ # @api public
21
+ # @example
22
+ # collection.empty? #=> false
23
+ # @return [Boolean] true if empty, false otherwise
24
+
25
+ # @!method first
26
+ # Returns the first element in the collection
27
+ # @api public
28
+ # @example
29
+ # collection.first #=> player1
30
+ # @return [Object, nil] the first element or nil if empty
31
+
32
+ # @!method last
33
+ # Returns the last element in the collection
34
+ # @api public
35
+ # @example
36
+ # collection.last #=> player3
37
+ # @return [Object, nil] the last element or nil if empty
38
+
39
+ # @!method length
40
+ # Returns the number of elements in the collection
41
+ # @api public
42
+ # @example
43
+ # collection.length #=> 3
44
+ # @return [Integer] the number of elements
45
+
46
+ # @!method count
47
+ # Returns the number of elements in the collection
48
+ # @api public
49
+ # @example
50
+ # collection.count #=> 3
51
+ # @return [Integer] the number of elements
52
+
53
+ def_delegators :elements, :size, :empty?, :first, :last
54
+
55
+ alias_method :length, :size
56
+ alias_method :count, :size
57
+
58
+ # Returns the elements in the collection
59
+ #
60
+ # @api private
61
+ # @return [Array] the elements
62
+ attr_reader :elements
63
+
64
+ # Initializes a new Collection
65
+ #
66
+ # @api public
67
+ # @example
68
+ # collection = NBA::Collection.new([player1, player2])
69
+ # @param elements [Array] the elements in the collection
70
+ # @return [NBA::Collection] a new collection instance
71
+ def initialize(elements = [])
72
+ @elements = elements
73
+ end
74
+
75
+ # Iterates over the elements in the collection
76
+ #
77
+ # @api public
78
+ # @example
79
+ # collection.each { |element| puts element }
80
+ # @yield [element] yields each element to the block
81
+ # @return [Enumerator, self] an enumerator or self
82
+ def each(&block)
83
+ return enum_for unless block
84
+
85
+ elements.each(&block)
86
+ self
87
+ end
88
+ end
89
+ end