advent_of_ruby 0.3.4 → 0.4.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 (213) hide show
  1. checksums.yaml +4 -4
  2. data/data/solutions/github/ZogStriP/2024/01_1.yml +1 -0
  3. data/data/solutions/github/ZogStriP/2024/01_2.yml +7 -0
  4. data/data/solutions/github/ZogStriP/2024/02_1.yml +1 -0
  5. data/data/solutions/github/ZogStriP/2024/02_2.yml +9 -0
  6. data/data/solutions/github/ZogStriP/2024/03_1.yml +1 -0
  7. data/data/solutions/github/ZogStriP/2024/03_2.yml +7 -0
  8. data/data/solutions/github/ZogStriP/2024/04_1.yml +1 -0
  9. data/data/solutions/github/ZogStriP/2024/04_2.yml +15 -0
  10. data/data/solutions/github/ZogStriP/2024/05_1.yml +1 -0
  11. data/data/solutions/github/ZogStriP/2024/05_2.yml +16 -0
  12. data/data/solutions/github/ZogStriP/2024/06_1.yml +1 -0
  13. data/data/solutions/github/ZogStriP/2024/06_2.yml +38 -0
  14. data/data/solutions/github/ZogStriP/2024/07_1.yml +1 -0
  15. data/data/solutions/github/ZogStriP/2024/07_2.yml +9 -0
  16. data/data/solutions/github/ZogStriP/2024/08_1.yml +1 -0
  17. data/data/solutions/github/ZogStriP/2024/08_2.yml +27 -0
  18. data/data/solutions/github/ZogStriP/2024/09_1.yml +1 -0
  19. data/data/solutions/github/ZogStriP/2024/09_2.yml +1 -0
  20. data/data/solutions/github/ZogStriP/2024/10_1.yml +1 -0
  21. data/data/solutions/github/ZogStriP/2024/10_2.yml +1 -0
  22. data/data/solutions/github/ZogStriP/2024/11_1.yml +1 -0
  23. data/data/solutions/github/ZogStriP/2024/11_2.yml +1 -0
  24. data/data/solutions/github/ZogStriP/2024/12_1.yml +1 -0
  25. data/data/solutions/github/ZogStriP/2024/12_2.yml +1 -0
  26. data/data/solutions/github/ZogStriP/2024/13_1.yml +1 -0
  27. data/data/solutions/github/ZogStriP/2024/13_2.yml +1 -0
  28. data/data/solutions/github/ZogStriP/2024/14_1.yml +1 -0
  29. data/data/solutions/github/ZogStriP/2024/14_2.yml +1 -0
  30. data/data/solutions/github/ZogStriP/2024/15_1.yml +1 -0
  31. data/data/solutions/github/ZogStriP/2024/15_2.yml +1 -0
  32. data/data/solutions/github/ZogStriP/2024/16_1.yml +1 -0
  33. data/data/solutions/github/ZogStriP/2024/16_2.yml +1 -0
  34. data/data/solutions/github/ZogStriP/2024/17_1.yml +1 -0
  35. data/data/solutions/github/ZogStriP/2024/17_2.yml +1 -0
  36. data/data/solutions/github/ZogStriP/2024/18_1.yml +1 -0
  37. data/data/solutions/github/ZogStriP/2024/18_2.yml +1 -0
  38. data/data/solutions/github/ZogStriP/2024/19_1.yml +1 -0
  39. data/data/solutions/github/ZogStriP/2024/19_2.yml +1 -0
  40. data/data/solutions/github/ZogStriP/2024/20_1.yml +1 -0
  41. data/data/solutions/github/ZogStriP/2024/20_2.yml +1 -0
  42. data/data/solutions/github/ZogStriP/2024/21_1.yml +1 -0
  43. data/data/solutions/github/ZogStriP/2024/21_2.yml +1 -0
  44. data/data/solutions/github/ZogStriP/2024/22_1.yml +1 -0
  45. data/data/solutions/github/ZogStriP/2024/22_2.yml +1 -0
  46. data/data/solutions/github/ZogStriP/2024/23_1.yml +1 -0
  47. data/data/solutions/github/ZogStriP/2024/23_2.yml +1 -0
  48. data/data/solutions/github/ZogStriP/2024/24_1.yml +1 -0
  49. data/data/solutions/github/ZogStriP/2024/24_2.yml +1 -0
  50. data/data/solutions/github/ZogStriP/2024/25_1.yml +1 -0
  51. data/data/solutions/github/ZogStriP/2025/01_1.yml +1 -0
  52. data/data/solutions/github/ZogStriP/2025/01_2.yml +1 -0
  53. data/data/solutions/github/ZogStriP/2025/02_1.yml +1 -0
  54. data/data/solutions/github/ZogStriP/2025/02_2.yml +1 -0
  55. data/data/solutions/github/ZogStriP/2025/03_1.yml +1 -0
  56. data/data/solutions/github/ZogStriP/2025/03_2.yml +1 -0
  57. data/data/solutions/github/ZogStriP/2025/04_1.yml +1 -0
  58. data/data/solutions/github/ZogStriP/2025/04_2.yml +1 -0
  59. data/data/solutions/github/ZogStriP/2025/05_1.yml +1 -0
  60. data/data/solutions/github/ZogStriP/2025/05_2.yml +1 -0
  61. data/data/solutions/github/ZogStriP/2025/06_1.yml +1 -0
  62. data/data/solutions/github/ZogStriP/2025/06_2.yml +1 -0
  63. data/data/solutions/github/ZogStriP/2025/07_1.yml +1 -0
  64. data/data/solutions/github/ZogStriP/2025/07_2.yml +1 -0
  65. data/data/solutions/github/ZogStriP/2025/08_1.yml +1 -0
  66. data/data/solutions/github/ZogStriP/2025/08_2.yml +1 -0
  67. data/data/solutions/github/ZogStriP/2025/09_1.yml +1 -0
  68. data/data/solutions/github/ZogStriP/2025/09_2.yml +1 -0
  69. data/data/solutions/github/ZogStriP/2025/10_1.yml +1 -0
  70. data/data/solutions/github/ZogStriP/2025/10_2.yml +1 -0
  71. data/data/solutions/github/ZogStriP/2025/11_1.yml +1 -0
  72. data/data/solutions/github/ZogStriP/2025/11_2.yml +1 -0
  73. data/data/solutions/github/ZogStriP/2025/12_1.yml +1 -0
  74. data/data/solutions/github/ZogStriP/2025/12_2.yml +1 -0
  75. data/data/solutions/github/ahorner/2025/01_1.yml +1 -0
  76. data/data/solutions/github/ahorner/2025/01_2.yml +37 -0
  77. data/data/solutions/github/ahorner/2025/02_1.yml +1 -0
  78. data/data/solutions/github/ahorner/2025/02_2.yml +41 -0
  79. data/data/solutions/github/ahorner/2025/03_1.yml +1 -0
  80. data/data/solutions/github/ahorner/2025/03_2.yml +24 -0
  81. data/data/solutions/github/ahorner/2025/04_1.yml +1 -0
  82. data/data/solutions/github/ahorner/2025/04_2.yml +39 -0
  83. data/data/solutions/github/ahorner/2025/05_1.yml +1 -0
  84. data/data/solutions/github/ahorner/2025/05_2.yml +48 -0
  85. data/data/solutions/github/ahorner/2025/06_1.yml +1 -0
  86. data/data/solutions/github/ahorner/2025/06_2.yml +44 -0
  87. data/data/solutions/github/ahorner/2025/07_1.yml +1 -0
  88. data/data/solutions/github/ahorner/2025/07_2.yml +56 -0
  89. data/data/solutions/github/ahorner/2025/08_1.yml +1 -0
  90. data/data/solutions/github/ahorner/2025/08_2.yml +59 -0
  91. data/data/solutions/github/ahorner/2025/09_1.yml +1 -0
  92. data/data/solutions/github/ahorner/2025/09_2.yml +101 -0
  93. data/data/solutions/github/ahorner/2025/10_1.yml +1 -0
  94. data/data/solutions/github/ahorner/2025/10_2.yml +72 -0
  95. data/data/solutions/github/ahorner/2025/11_1.yml +1 -0
  96. data/data/solutions/github/ahorner/2025/11_2.yml +45 -0
  97. data/data/solutions/github/ahorner/2025/12_1.yml +1 -0
  98. data/data/solutions/github/eregon/2025/01_1.yml +17 -0
  99. data/data/solutions/github/eregon/2025/01_2.yml +14 -0
  100. data/data/solutions/github/eregon/2025/02_1.yml +22 -0
  101. data/data/solutions/github/eregon/2025/02_2.yml +58 -0
  102. data/data/solutions/github/eregon/2025/03_1.yml +13 -0
  103. data/data/solutions/github/eregon/2025/03_2.yml +13 -0
  104. data/data/solutions/github/eregon/2025/04_1.yml +14 -0
  105. data/data/solutions/github/eregon/2025/04_2.yml +16 -0
  106. data/data/solutions/github/eregon/2025/05_1.yml +1 -0
  107. data/data/solutions/github/eregon/2025/05_2.yml +1 -0
  108. data/data/solutions/github/eregon/2025/06_1.yml +1 -0
  109. data/data/solutions/github/eregon/2025/06_2.yml +1 -0
  110. data/data/solutions/github/eregon/2025/07_1.yml +1 -0
  111. data/data/solutions/github/eregon/2025/07_2.yml +1 -0
  112. data/data/solutions/github/eregon/2025/08_1.yml +1 -0
  113. data/data/solutions/github/eregon/2025/08_2.yml +1 -0
  114. data/data/solutions/github/eregon/2025/09_1.yml +1 -0
  115. data/data/solutions/github/eregon/2025/09_2.yml +1 -0
  116. data/data/solutions/github/eregon/2025/10_1.yml +1 -0
  117. data/data/solutions/github/eregon/2025/10_2.yml +1 -0
  118. data/data/solutions/github/eregon/2025/11_1.yml +1 -0
  119. data/data/solutions/github/eregon/2025/11_2.yml +1 -0
  120. data/data/solutions/github/eregon/2025/12_1.yml +1 -0
  121. data/data/solutions/github/erikw/2025/01_1.yml +1 -0
  122. data/data/solutions/github/erikw/2025/01_2.yml +1 -0
  123. data/data/solutions/github/erikw/2025/02_1.yml +1 -0
  124. data/data/solutions/github/erikw/2025/02_2.yml +1 -0
  125. data/data/solutions/github/erikw/2025/03_1.yml +1 -0
  126. data/data/solutions/github/erikw/2025/03_2.yml +1 -0
  127. data/data/solutions/github/erikw/2025/04_1.yml +1 -0
  128. data/data/solutions/github/erikw/2025/04_2.yml +1 -0
  129. data/data/solutions/github/erikw/2025/05_1.yml +1 -0
  130. data/data/solutions/github/erikw/2025/05_2.yml +1 -0
  131. data/data/solutions/github/erikw/2025/06_1.yml +1 -0
  132. data/data/solutions/github/erikw/2025/06_2.yml +1 -0
  133. data/data/solutions/github/erikw/2025/07_1.yml +1 -0
  134. data/data/solutions/github/erikw/2025/07_2.yml +1 -0
  135. data/data/solutions/github/erikw/2025/08_1.yml +1 -0
  136. data/data/solutions/github/erikw/2025/08_2.yml +1 -0
  137. data/data/solutions/github/erikw/2025/09_1.yml +1 -0
  138. data/data/solutions/github/erikw/2025/09_2.yml +1 -0
  139. data/data/solutions/github/erikw/2025/10_1.yml +1 -0
  140. data/data/solutions/github/erikw/2025/10_2.yml +1 -0
  141. data/data/solutions/github/erikw/2025/11_1.yml +1 -0
  142. data/data/solutions/github/erikw/2025/11_2.yml +1 -0
  143. data/data/solutions/github/erikw/2025/12_1.yml +1 -0
  144. data/data/solutions/github/erikw/2025/12_2.yml +1 -0
  145. data/data/solutions/github/gchan/2025/01_1.yml +25 -0
  146. data/data/solutions/github/gchan/2025/01_2.yml +32 -0
  147. data/data/solutions/github/gchan/2025/02_1.yml +18 -0
  148. data/data/solutions/github/gchan/2025/02_2.yml +21 -0
  149. data/data/solutions/github/gchan/2025/03_1.yml +15 -0
  150. data/data/solutions/github/gchan/2025/03_2.yml +23 -0
  151. data/data/solutions/github/gchan/2025/04_1.yml +27 -0
  152. data/data/solutions/github/gchan/2025/04_2.yml +40 -0
  153. data/data/solutions/github/gchan/2025/05_1.yml +20 -0
  154. data/data/solutions/github/gchan/2025/05_2.yml +36 -0
  155. data/data/solutions/github/gchan/2025/06_1.yml +11 -0
  156. data/data/solutions/github/gchan/2025/06_2.yml +28 -0
  157. data/data/solutions/github/gchan/2025/07_1.yml +33 -0
  158. data/data/solutions/github/gchan/2025/07_2.yml +29 -0
  159. data/data/solutions/github/gchan/2025/08_1.yml +48 -0
  160. data/data/solutions/github/gchan/2025/08_2.yml +48 -0
  161. data/data/solutions/github/gchan/2025/09_1.yml +16 -0
  162. data/data/solutions/github/gchan/2025/09_2.yml +60 -0
  163. data/data/solutions/github/gchan/2025/10_1.yml +49 -0
  164. data/data/solutions/github/gchan/2025/10_2.yml +154 -0
  165. data/data/solutions/github/gchan/2025/11_1.yml +43 -0
  166. data/data/solutions/github/gchan/2025/11_2.yml +33 -0
  167. data/data/solutions/github/gchan/2025/12_1.yml +51 -0
  168. data/data/solutions/reddit/ruby/2017/06.yml +0 -2
  169. data/data/solutions/reddit/ruby/2020/02.yml +0 -1
  170. data/data/solutions/reddit/ruby/2024/02.yml +0 -1
  171. data/data/solutions/reddit/ruby/2025/01.yml +187 -0
  172. data/data/solutions/reddit/ruby/2025/02.yml +185 -0
  173. data/data/solutions/reddit/ruby/2025/03.yml +369 -0
  174. data/data/solutions/reddit/ruby/2025/04.yml +217 -0
  175. data/data/solutions/reddit/ruby/2025/05.yml +324 -0
  176. data/data/solutions/reddit/ruby/2025/06.yml +246 -0
  177. data/data/solutions/reddit/ruby/2025/07.yml +213 -0
  178. data/data/solutions/reddit/ruby/2025/08.yml +73 -0
  179. data/data/solutions/reddit/ruby/2025/09.yml +26 -0
  180. data/data/solutions/reddit/ruby/2025/10.yml +73 -0
  181. data/data/solutions/reddit/ruby/2025/11.yml +69 -0
  182. data/data/solutions/reddit/ruby/2025/12.yml +1 -0
  183. data/lib/arb/arb.rb +0 -5
  184. data/lib/arb/cli/bootstrap.rb +1 -1
  185. data/lib/arb/cli/commit.rb +1 -1
  186. data/lib/arb/cli/progress.rb +4 -8
  187. data/lib/arb/cli/run.rb +1 -1
  188. data/lib/arb/cli/shared/git.rb +3 -4
  189. data/lib/arb/cli/shared/year_day_validator.rb +3 -8
  190. data/lib/arb/files/spec.rb +2 -2
  191. data/lib/arb/formatters/rubocop.rb +18 -11
  192. data/lib/arb/util.rb +58 -0
  193. data/lib/arb/version.rb +1 -1
  194. metadata +184 -23
  195. data/lib/download_solutions/api/github/repos.rb +0 -54
  196. data/lib/download_solutions/api/github.rb +0 -164
  197. data/lib/download_solutions/api/reddit/add_missing_replies.rb +0 -43
  198. data/lib/download_solutions/api/reddit/clean_bodies.rb +0 -64
  199. data/lib/download_solutions/api/reddit/filter_by_language.rb +0 -32
  200. data/lib/download_solutions/api/reddit/get_initial_response.rb +0 -30
  201. data/lib/download_solutions/api/reddit/get_serial_comments.rb +0 -145
  202. data/lib/download_solutions/api/reddit/megathread_ids.rb +0 -19
  203. data/lib/download_solutions/api/reddit/params.rb +0 -40
  204. data/lib/download_solutions/api/reddit/reject_unwanted_replies.rb +0 -31
  205. data/lib/download_solutions/api/reddit/remove_ids.rb +0 -26
  206. data/lib/download_solutions/api/reddit/remove_language_tags.rb +0 -29
  207. data/lib/download_solutions/api/reddit.rb +0 -101
  208. data/lib/download_solutions/cli/cli/shared.rb +0 -35
  209. data/lib/download_solutions/cli/github.rb +0 -107
  210. data/lib/download_solutions/cli/reddit.rb +0 -64
  211. data/lib/download_solutions/download_solutions.rb +0 -18
  212. data/lib/download_solutions/reverse_markdown/converters/br.rb +0 -15
  213. data/lib/download_solutions/reverse_markdown/converters/pre.rb +0 -46
@@ -1,32 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- class FilterByLanguage
5
- # Filters comments by the desired programming language(s).
6
- #
7
- # @param params [Reddit::Params]
8
- # @return [Array<Hash>]
9
- def self.call(params:)
10
- params.original_comments.filter { |comment|
11
- comment_body = comment[:body]&.downcase
12
- next unless comment_body
13
-
14
- language_specified = comment_body.match?(/\[[[:punct:]]*language:/i)
15
-
16
- if language_specified
17
- params.languages.any? { |language|
18
- comment_body.match?(/\[[[:punct:]]*language:\s*#{language}/i)
19
- }
20
- else
21
- params.languages.any? { |language|
22
- # "sh:" because of "<pre class=\"brush:ruby", see:
23
- # https://github.com/xijo/reverse_markdown/blob/14d53d5f914fd926b49e6492fd7bd95e62ef541a/lib/reverse_markdown/converters/pre.rb#L37
24
- comment_body.match?(/(?<!```|sh:)#{language}/i)
25
- }
26
- end
27
- }
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,30 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- class GetInitialResponse
5
- # Equivalent to the initial page load of a Reddit thread, which loads
6
- # some comments, but (if the thread has many comments) not all of them.
7
- #
8
- # @param params [Reddit::Params]
9
- # @return [Faraday::Response]
10
- def self.call(params:)
11
- initial_response = nil
12
-
13
- loop do
14
- initial_response = params.connection.get(params.megathread_path)
15
-
16
- if initial_response.body.empty?
17
- puts PASTEL.bright_black("Throttled by Reddit. Sleeping for 60 seconds...")
18
- sleep 60
19
- else
20
- puts "Fetching comments for #{params.year}##{params.day.to_s.rjust(2, "0")}..."
21
- break
22
- end
23
- end
24
-
25
- initial_response
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,145 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- class GetSerialComments
5
- MAX_SLEEP_COUNT = 10
6
-
7
- # Equivalent to repeatedly pressing "View more comments" in a thread's top
8
- # level (or the "+" below a comment) until reaching the end. "Serial"
9
- # because this doesn't fetch all replies; many (not all) replies are in
10
- # "more children" nodes and not yet fetched.
11
- #
12
- # @param params [Reddit::Params]
13
- # @param parent_id [String] the ID of the parent, by default the thread.
14
- # @return [Array(Array<Hash>, Array<Hash>)] comments and "more children" nodes.
15
- #
16
- # @raise [MaxSleepCountReachedError] if Reddit seemingly throttled for an
17
- # unusually long time, "seemingly" because the only sign is an empty
18
- # JSON response body after loading additional comments.
19
- # @raise [MultipleMoreChildrensError] if there are multiple "more children"
20
- # nodes for the thread or a comment; only one at a time is expected.
21
- # If there are ever more, this algorithm will need to change from a
22
- # serial loop to recursion.
23
- def self.call(params:, parent_id: params.thread_id)
24
- comments = initial_comments_or_replies(params, parent_id) || (return [[], []])
25
-
26
- loop do
27
- more_top_level_childrens, comments = comments.partition {
28
- it[:children] && it[:parent_id] == parent_id
29
- }
30
- break unless more_top_level_childrens.any?
31
- raise Reddit::MultipleMoreChildrensError if more_top_level_childrens.count > 1
32
-
33
- # Loop again to fetch more if there are more top-level comments.
34
- comments += fetch_more_children(params, more_top_level_childrens.first, parent_id)
35
- end
36
-
37
- more_childrens, comments = comments.partition { it[:children] }
38
-
39
- [comments, more_childrens]
40
- end
41
-
42
- private_class_method def self.initial_comments_or_replies(params, parent_id)
43
- if parent_id == params.thread_id
44
- parse_initial_response(params)
45
- else
46
- more_childrens = params.more_childrens.select {
47
- it[:parent_id] == parent_id
48
- }
49
- raise Reddit::MultipleMoreChildrensError if more_childrens.count > 1
50
-
51
- if more_childrens.empty?
52
- # signal to return empty arrays for comments and more_childrens
53
- return nil
54
- end
55
-
56
- fetch_more_children(params, more_childrens.first, parent_id)
57
- end
58
- end
59
-
60
- private_class_method def self.parse_initial_response(params)
61
- simplify_comments(
62
- JSON.parse(params.initial_response.body).dig(-1, "data", "children")
63
- )
64
- end
65
-
66
- private_class_method def self.fetch_more_children(params, more_children, parent_id)
67
- response = nil
68
- sleep_count = 0
69
- loop do
70
- # POST because a GET request would sometimes be too long.
71
- response = params.connection.post(
72
- "/api/morechildren.json",
73
- "link_id=#{params.thread_id}" \
74
- "&children=#{more_children[:children].join(",")}"
75
- )
76
-
77
- if response.body.empty?
78
- if sleep_count < MAX_SLEEP_COUNT
79
- puts PASTEL.bright_black("Throttled by Reddit. Sleeping for 60 seconds...")
80
- sleep_count += 1
81
- sleep 60
82
- else
83
- raise Reddit::MaxSleepCountReachedError
84
- end
85
- else
86
- puts "Continuing to fetch comments..." if sleep_count > 1
87
- break
88
- end
89
- end
90
-
91
- simplify_comments(
92
- JSON.parse(response.body).dig("jquery", 10, 3, 0)
93
- )
94
- end
95
-
96
- private_class_method def self.simplify_comments(raw_comments)
97
- raw_more_childrens, raw_comments = raw_comments.partition { it["kind"] == "more" }
98
-
99
- comments = raw_comments.map { |raw_comment|
100
- simplify_comment(raw_comment, raw_more_childrens)
101
- }
102
-
103
- more_childrens = raw_more_childrens.filter_map {
104
- if it["data"]["children"].any?
105
- {
106
- children: it["data"]["children"],
107
- parent_id: it["data"]["parent_id"]
108
- }
109
- end
110
- }
111
-
112
- comments + more_childrens
113
- end
114
-
115
- private_class_method def self.simplify_comment(raw_comment, raw_more_childrens)
116
- {
117
- author: raw_comment["data"]["author"],
118
- url: "https://www.reddit.com#{raw_comment["data"]["permalink"]}".delete_suffix("/"),
119
- body: raw_comment["data"]["body"],
120
- id: raw_comment["data"]["name"],
121
- parent_id: raw_comment["data"]["parent_id"],
122
- replies: simplify_replies(raw_comment, raw_more_childrens)
123
- }
124
- end
125
-
126
- private_class_method def self.simplify_replies(raw_comment, raw_more_childrens)
127
- return [] if raw_comment["data"]["replies"].nil? || raw_comment["data"]["replies"].empty?
128
-
129
- raw_comment.dig("data", "replies", "data", "children").filter_map { |child|
130
- if child["kind"] == "more"
131
- # Move "more children" nodes (listing additional replies that are
132
- # not yet fetched) out to an array that is appended onto the
133
- # upper-level comments (see #simplify_comments), so that they can
134
- # all be dealt with together by #add_missing_replies!
135
- raw_more_childrens << child
136
- next nil
137
- end
138
-
139
- simplify_comment(child, raw_more_childrens)
140
- }
141
- end
142
- end
143
- end
144
- end
145
- end
@@ -1,19 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- # From https://www.reddit.com/r/adventofcode/wiki/archives/solution_megathreads
5
- MEGATHREAD_IDS = {
6
- 2024 => %w[1h3vp6n 1h4ncyr 1h5frsp 1h689qf 1h71eyz 1h7tovg 1h8l3z5 1h9bdmp 1ha27bo 1hau6hl 1hbm0al 1hcdnk0 1hd4wda 1hdvhvu 1hele8m 1hfboft 1hg38ah 1hguacy 1hhlb8g 1hicdtb 1hj2odw 1hjroap 1hkgj5b 1hl698z 1hlu4ht],
7
- 2023 => %w[1883ibu 188w447 189m3qw 18actmy 18b4b0r 18bwe6t 18cnzbm 18df7px 18e5ytd 18evyu9 18fmrjk 18ge41g 18h940b 18i0xtn 18isayp 18jjpfk 18k9ne5 18l0qtr 18ltr8m 18mmfxb 18nevo3 18o7014 18oy4pc 18pnycy 18qbsxs],
8
- 2022 => %w[z9ezjb zac2v2 zb865p zc0zta zcxid5 zdw0u6 zesk40 zfpnka zgnice zhjfo4 zifqmh zjnruc zkmyh4 zli1rd zmcn64 zn6k1l znykq2 zoqhvy zpihwi zqezkn zrav4h zsct8w zt6xz5 zu28ij zur1an],
9
- 2021 => %w[r66vow r6zd93 r7r0ff r8i1lq r9824c r9z49j rar7ty rbj87a rca6vp rd0s54 rds32p rehj2r rf7onx rfzq6f rgqzt5 rhj2hm ri9kdq rizw2c rjpf7f rkf5ek rl6p8y rlxhmg rmnozs rnejv5 ro2uav],
10
- 2020 => %w[k4e4lm k52psu k5qsrk k6e8sw k71h6r k7ndux k8a31f k8xw8h k9lfwj ka8z8x kaw6oz kbj5me kc4njx kcr1ct kdf85p ke2qp6 keqsfa kfeldk kg1mro kgo01p khaiyk khyjgv kimluc kj96iw kjtg7y],
11
- 2019 => %w[e4axxe e4u0rw e5bz2w e5u5fv e6carb e6tyva e7a4nj e7pkmt e85b6d e8m1z3 e92jm2 e9j0ve e9zgse eafj32 eaurfo ebai4g ebr7dg ec8090 ecogl3 ed5ei2 edll5a ee0rqi eefva8 eewjtt efca4m],
12
- 2018 => %w[a20646 a2aimr a2lesz a2xef8 a3912m a3kr4r a3wmnl a47ubw a4i97s a4skra a53r6i a5eztl a5qd71 a61ojp a6chwa a6mf8a a6wpup a77xq6 a7j9zc a7uk3f a86jgt a8i1cy a8s17l a91ysq a9c61w],
13
- 2017 => %w[7gsrc2 7h0rnm 7h7ufl 7hf5xb 7hngbn 7hvtoq 7i44pg 7icnff 7iksqc 7irzg5 7izym2 7j89tr 7jgyrt 7jpelc 7jxkiw 7k572l 7kc0xw 7kj35s 7kr2ac 7kz6ik 7l78eb 7lf943 7lms6p 7lte5z 7lzo3l],
14
- 2016 => %w[5fur6q 5g1hfm 5g80ck 5gdvve 5gk2yv 5gr0xf 5gy1f2 5h52ro 5hbygy 5hijk5 5hoia9 5hus40 5i1q0h 5i8pzz 5ifn4v 5imh3d 5isvxv 5iyp50 5j4lp1 5jbeqo 5ji29h 5jor9q 5jvbzt 5k1he1 5k6yfu],
15
- 2015 => %w[3uyl7s 3v3w2f 3v8roh 3vdn8a 3viazx 3vmltn 3vr4m4 3vw32y 3w192e 3w6h3m 3wbzyv 3wh73d 3wm0oy 3wqtx2 3wwj84 3x1i26 3x6cyr 3xb3cj 3xflz8 3xjpp2 3xnyoi 3xspyl 3xxdxt 3y1s7f 3y5jco]
16
- }
17
- end
18
- end
19
- end
@@ -1,40 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- # A parameter object that is passed into each step within Reddit#get_comments.
5
- class Params
6
- attr_reader :year, :day, :languages, :connection, :megathread_path, :thread_id
7
- attr_accessor :initial_response, :original_comments, :more_childrens, :comments
8
-
9
- # @param year [Integer]
10
- # @param day [Integer]
11
- # @param languages [Array<String>] e.g. ["ruby"]
12
- def initialize(year:, day:, languages:, connection:)
13
- @year = year
14
- @day = day
15
- @languages = languages
16
- @connection = connection
17
- @megathread_path = build_megathread_path(year:, day:)
18
- @thread_id = "t3_#{megathread_id(year:, day:)}"
19
- end
20
-
21
- private
22
-
23
- def build_megathread_path(year:, day:)
24
- if year == 2015 && day == 1
25
- return "/r/programming/comments/#{Reddit::MEGATHREAD_IDS[2015][0]}/daily_programming_puzzles_at_advent_of_code.json"
26
- end
27
-
28
- slug = "day_#{day.to_i}_solutions"
29
- slug = "#{year.to_i}_#{slug}" if year > 2015
30
-
31
- "/r/adventofcode/comments/#{megathread_id(year:, day:)}/#{slug}.json"
32
- end
33
-
34
- def megathread_id(year:, day:)
35
- Reddit::MEGATHREAD_IDS[year.to_i][day - 1]
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,31 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- class RejectUnwantedReplies
5
- # Filters out replies that are not gennerally relevant for posterity:
6
- # - removed/deleted with no replies
7
- # - by moderators
8
- # - by bots
9
- #
10
- # @param params [Reddit::Params]
11
- # @return [void] params.comments is modified in place.
12
- def self.call(params:)
13
- params.comments.each do |comment|
14
- reject_unwanted_replies!(comment)
15
- end
16
- end
17
-
18
- private_class_method def self.reject_unwanted_replies!(comment)
19
- comment[:replies].each do |reply|
20
- reject_unwanted_replies!(reply)
21
- end
22
-
23
- comment[:replies].reject! do
24
- (["[removed]", "[deleted]"].include?(it[:body].strip) && it[:replies].empty?) ||
25
- %w[AutoModerator daggerdragon backtickbot].include?(it[:author])
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,26 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- class RemoveIds
5
- # Removes IDs from comments.
6
- #
7
- # @param params [Reddit::Params]
8
- # @return [void] params.comments is modified in place.
9
- def self.call(params:)
10
- params.comments.each do |comment|
11
- remove_ids!(comment)
12
- end
13
- end
14
-
15
- private_class_method def self.remove_ids!(comment)
16
- comment.delete(:id)
17
- comment.delete(:parent_id)
18
-
19
- comment[:replies].each do |reply|
20
- remove_ids!(reply)
21
- end
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,29 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- class RemoveLanguageTags
5
- # Removes language tags from comment bodies.
6
- #
7
- # @param params [Reddit::Params]
8
- # @return [void] params.comments is modified in place.
9
- def self.call(params:)
10
- params.comments.each do |comment|
11
- remove_language_tag!(comment, params.languages)
12
- end
13
- end
14
-
15
- private_class_method def self.remove_language_tag!(comment, languages)
16
- languages.each do |language|
17
- comment[:body].sub!(/\[[[:punct:]]*language:\s*#{language}[[:punct:]]*\]/i, "")
18
- end
19
-
20
- comment[:body].strip!
21
-
22
- comment[:replies].each do |reply|
23
- remove_language_tag!(reply, languages)
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,101 +0,0 @@
1
- module DownloadSolutions
2
- module Api
3
- class Reddit
4
- MaxSleepCountReachedError = Class.new(StandardError)
5
- MultipleMoreChildrensError = Class.new(StandardError)
6
-
7
- private attr_reader :user_agent, :client_id, :client_secret, :username, :password
8
-
9
- # @param client_id [String]
10
- # @param client_secret [String]
11
- # @param username [String]
12
- # @param password [String]
13
- def initialize(client_id:, client_secret:, username:, password:)
14
- @user_agent = "AdventOfRubyScript/#{Arb::VERSION} by fpsvogel"
15
- @client_id = client_id
16
- @client_secret = client_secret
17
- @username = username
18
- @password = password
19
- end
20
-
21
- # @param year [Integer]
22
- # @param day [Integer]
23
- # @param languages [Array<String>] e.g. ["ruby"]
24
- # @return [Array<Hash>]
25
- #
26
- # @raise [MaxSleepCountReachedError] if Reddit seemingly throttled for an
27
- # unusually long time, "seemingly" because the only sign is an empty
28
- # JSON response body after loading additional comments.
29
- # @raise [MultipleMoreChildrensError] if there are multiple "more children"
30
- # nodes for the thread or a comment; only one at a time is expected.
31
- def get_comments(year:, day:, languages:)
32
- params = Params.new(
33
- year:,
34
- day:,
35
- languages:,
36
- connection:
37
- )
38
-
39
- initial_response = GetInitialResponse.call(params:)
40
- params.initial_response = initial_response
41
-
42
- # Keep unfetched replies ("more children" nodes) separate so that after
43
- # filtering, the replies to filtered-in comments can then be fetched.
44
- original_comments, more_childrens = GetSerialComments.call(params:)
45
- params.original_comments = original_comments
46
- params.more_childrens = more_childrens
47
-
48
- filtered_comments = FilterByLanguage.call(params:)
49
- params.comments = filtered_comments
50
-
51
- # These operations modify params#comments in place.
52
- AddMissingReplies.call(params:)
53
- RejectUnwantedReplies.call(params:)
54
- CleanBodies.call(params:)
55
- RemoveLanguageTags.call(params:)
56
- RemoveIds.call(params:)
57
-
58
- params.comments
59
- end
60
-
61
- private
62
-
63
- def connection
64
- @connection ||= Faraday.new(
65
- url: "https://oauth.reddit.com",
66
- headers: {
67
- "User-Agent" => user_agent,
68
- "Accept" => "application/json"
69
- }
70
- ) do |f|
71
- f.request :authorization, "Bearer", -> { auth_token }
72
- f.request :retry, {
73
- max: 5,
74
- interval: 0.5,
75
- interval_randomness: 0.5,
76
- backoff_factor: 2
77
- }
78
- end
79
- end
80
-
81
- def auth_token
82
- connection = Faraday.new(
83
- url: "https://www.reddit.com",
84
- headers: {
85
- "User-Agent" => user_agent
86
- }
87
- ) do |f|
88
- f.request :authorization, :basic, client_id, client_secret
89
- f.response :json
90
- end
91
-
92
- response = connection.post(
93
- "/api/v1/access_token",
94
- "grant_type=password&username=#{username}&password=#{password}"
95
- )
96
-
97
- response.body["access_token"]
98
- end
99
- end
100
- end
101
- end
@@ -1,35 +0,0 @@
1
- module DownloadSolutions
2
- module Cli
3
- private_class_method def self.validate_year_and_day(year:, day:)
4
- if day
5
- if year.nil?
6
- raise InputError, "Year must be specified when day is specified."
7
- end
8
- if !day.between?(1, 25) && Date.new(year, 12, day) > Date.today
9
- raise InputError, "Day must be between 1 and 25, and <= today."
10
- end
11
- end
12
- if year && !year.between?(2015, Date.today.year)
13
- raise InputError, "Year must be between 2015 and this year."
14
- end
15
- end
16
-
17
- private_class_method def self.max_year_and_day(year:, day:)
18
- if Date.today.year == year && Date.today.month == 12
19
- [Date.today.year, Date.today.day]
20
- else
21
- [Date.today.year - 1, 25]
22
- end
23
- end
24
-
25
- private_class_method def self.output_initial_message(source:, year:, day:, force:, detail:)
26
- force_description = PASTEL.red("FORCE ") if force
27
- year_description = year.nil? ? "all years" : year.to_s
28
- day_description = day.nil? ? "" : "##{day.to_s.rjust(2, "0")}"
29
- time_description = PASTEL.blue("#{year_description}#{day_description}")
30
-
31
- puts "#{force_description}Downloading #{source} solutions from #{time_description} #{detail}..."
32
- puts
33
- end
34
- end
35
- end
@@ -1,107 +0,0 @@
1
- module DownloadSolutions
2
- module Cli
3
- def self.github(year: nil, day: nil, author: nil, force: false)
4
- validate_year_and_day(year:, day:)
5
-
6
- author_name = author.nil? ? "all authors" : PASTEL.blue(author)
7
- detail = "by #{author_name}"
8
- output_initial_message(source: "GitHub", year:, day:, force:, detail:)
9
-
10
- github_directory = File.join("data", "solutions", "github")
11
-
12
- repos = Api::Github::REPOS
13
- if author
14
- if repos.key?(author)
15
- repos = repos.select { it == author }
16
- else
17
- raise InputError, "Repo author #{PASTEL.blue(author)} not found."
18
- end
19
- end
20
-
21
- max_year, max_day = max_year_and_day(year:, day:)
22
-
23
- authors = repos.keys
24
- authors.each do |author|
25
- author_directory = File.join(github_directory, author)
26
- Dir.mkdir(author_directory) unless Dir.exist?(author_directory)
27
-
28
- (year || 2015).upto(year || max_year) do |current_year|
29
- year_directory = File.join(author_directory, current_year.to_s)
30
-
31
- existing_solutions =
32
- if Dir.exist?(year_directory)
33
- Dir.entries(year_directory)
34
- .filter_map {
35
- it.delete_suffix(".yml").split("_").map(&:to_i) if it.end_with?(".yml")
36
- }.sort
37
- else
38
- []
39
- end
40
-
41
- solutions = github_api.get_solutions(
42
- author:,
43
- year: current_year,
44
- input_day: day,
45
- max_day:,
46
- force:,
47
- existing_solutions:
48
- )
49
-
50
- skipped_list = solutions_str(solutions[:skipped])
51
- if solutions[:skipped].any?
52
- puts "#{PASTEL.yellow.bold("Skipping")} from #{author} #{current_year}, already existing: #{PASTEL.yellow(skipped_list)}"
53
- end
54
-
55
- not_found_list = solutions_str(solutions[:not_found])
56
- if solutions[:not_found].any?
57
- puts "#{PASTEL.red.bold("Not found")} from #{author} #{current_year}: #{PASTEL.red(not_found_list)}"
58
- if solutions[:not_found].size < 49
59
- # Save empty files for not found solutions, so that they won't be
60
- # attempted to be downloaded again.
61
- Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
62
- solutions[:not_found].each do |(day, part)|
63
- path = File.join(year_directory, "#{day.to_s.rjust(2, "0")}_#{part}.yml")
64
- File.write(path, [].to_yaml)
65
- end
66
- end
67
- end
68
-
69
- if solutions[:new].any?
70
- Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
71
- solutions[:new].each do |(day, part), content|
72
- path = File.join(year_directory, "#{day.to_s.rjust(2, "0")}_#{part}.yml")
73
- File.write(path, content.to_yaml(line_width: -1))
74
- end
75
-
76
- new_list = solutions_str(solutions[:new].keys)
77
- puts "#{PASTEL.blue.bold("Saved")} from #{author} #{current_year}: #{PASTEL.blue(new_list)}"
78
- puts "Saved to #{year_directory}"
79
- end
80
- end
81
-
82
- puts
83
- end
84
- rescue InputError => e
85
- puts PASTEL.red(e.message)
86
- end
87
-
88
- private_class_method def self.solutions_str(solutions)
89
- if solutions.count == 49
90
- "all"
91
- else
92
- solutions.sort.map { it.join(".") }.join(", ")
93
- end
94
- end
95
-
96
- private_class_method def self.github_api
97
- return @github_api if @github_api
98
-
99
- github_token = "GITHUB_TOKEN"
100
-
101
- Dotenv.load
102
- Dotenv.require_keys([github_token])
103
-
104
- @github_api = Api::Github.new(auth_token: ENV[github_token])
105
- end
106
- end
107
- end
@@ -1,64 +0,0 @@
1
- module DownloadSolutions
2
- module Cli
3
- def self.reddit(year: nil, day: nil, languages: ["ruby"], force: false)
4
- validate_year_and_day(year:, day:)
5
-
6
- language_names = PASTEL.blue(languages.join(", "))
7
- detail = "for #{language_names}"
8
- output_initial_message(source: "Reddit", year:, day:, force:, detail:)
9
-
10
- language_directory = File.join("data", "solutions", "reddit", languages.join("-"))
11
- Dir.mkdir(language_directory) unless Dir.exist?(language_directory)
12
-
13
- max_year, max_day = max_year_and_day(year:, day:)
14
-
15
- (year || 2015).upto(year || max_year) do |current_year|
16
- year_directory = File.join("data", "solutions", "reddit", languages.join("-"), current_year.to_s)
17
- Dir.mkdir(year_directory) unless Dir.exist?(year_directory)
18
-
19
- (day || 1).upto(day || max_day) do |current_day|
20
- path = File.join("data", "solutions", "reddit", languages.join("-"), year.to_s, "#{current_day.to_s.rjust(2, "0")}.yml")
21
-
22
- if File.exist?(path) && !force
23
- puts PASTEL.yellow("Skipping #{PASTEL.yellow.bold("#{current_year}##{current_day.to_s.rjust(2, "0")}")} as it already exists.")
24
- puts
25
- next
26
- end
27
-
28
- comments = reddit_api.get_comments(
29
- year: current_year,
30
- day: current_day,
31
- languages:
32
- )
33
-
34
- File.write(path, comments.to_yaml(line_width: -1))
35
-
36
- puts "Saved #{PASTEL.blue("#{current_year}##{current_day.to_s.rjust(2, "0")}")} to #{path}"
37
- puts
38
- end
39
- end
40
- rescue InputError => e
41
- puts PASTEL.red(e.message)
42
- end
43
-
44
- private_class_method def self.reddit_api
45
- return @reddit_api if @reddit_api
46
-
47
- reddit_api_keys = %w[
48
- REDDIT_CLIENT_ID
49
- REDDIT_CLIENT_SECRET
50
- REDDIT_USERNAME
51
- REDDIT_PASSWORD
52
- ]
53
-
54
- Dotenv.load
55
- Dotenv.require_keys(reddit_api_keys)
56
-
57
- reddit_api_kwargs = %i[client_id client_secret username password]
58
- .zip(reddit_api_keys.map { ENV[it] })
59
- .to_h
60
-
61
- @reddit_api = Api::Reddit.new(**reddit_api_kwargs)
62
- end
63
- end
64
- end