github_heatmap/heatmap/
contribution_week.rs1use scraper::ElementRef;
2use crate::HeatmapError;
3use super::Contribution;
4
5const Y_ATTR: &str = "y";
6
7#[derive(Debug, Eq, PartialEq)]
15pub struct ContributionWeek {
16 pub contributions: Vec<Option<Contribution>>
22}
23
24impl ContributionWeek {
25 pub fn from_days(days: &Vec<ElementRef>) -> Result<Self, HeatmapError> {
40 let mut contributions: Vec<Option<Contribution>> = vec![None; 7];
41
42 for day in days {
43 let y_value = Self::parse_y_attr(day)?;
44 let day_index = Self::get_day_index(y_value)?;
45 let contribution = Contribution::from_el(day)?;
46
47 contributions[day_index] = Some(contribution);
48 }
49
50 Ok(ContributionWeek { contributions })
51 }
52
53 fn parse_y_attr(day_el: &ElementRef) -> Result<usize, HeatmapError> {
54 let result = day_el
55 .value()
56 .attr(Y_ATTR)
57 .ok_or_else(|| HeatmapError::QueryAttribute {
58 attr: Y_ATTR.to_string(),
59 on_alias: "heatmap node".to_string(),
60 })?
61 .parse()
62 .map_err(|_| HeatmapError::ParseAttribute {
63 attr: Y_ATTR.to_string(),
64 on_alias: "heatmap node".to_string(),
65 })?;
66
67 Ok(result)
68 }
69
70 fn get_day_index(y_value: usize) -> Result<usize, HeatmapError> {
71 match y_value {
74 0 => Ok(0),
75 y if y % 13 == 0 => Ok(y / 13),
76 y if y % 15 == 0 => Ok(y / 15),
77 _ => Err(HeatmapError::UnknownNodeFormat)
78 }
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use scraper::{Html, Selector};
86
87 #[test]
88 fn constructs_contribution_week() {
89 let fragment = Html::parse_fragment(r#"
90 <rect y='0' data-level='1' />
91 <rect y='15' data-level='2' />
92 <rect y='30' data-level='3' />
93 <rect y='45' data-level='4' />
94 <rect y='60' data-level='4' />
95 <rect y='75' data-level='4' />
96 <rect y='90' data-level='4' />
97 "#);
98
99 let selector = Selector::parse("rect").unwrap();
100 let rects: Vec<_> = fragment.select(&selector).collect();
101 let contribution_week = ContributionWeek::from_days(&rects).unwrap();
102
103 let expected = ContributionWeek {
104 contributions: vec![
105 Some(Contribution { heat_level: 1 }),
106 Some(Contribution { heat_level: 2 }),
107 Some(Contribution { heat_level: 3 }),
108 Some(Contribution { heat_level: 4 }),
109 Some(Contribution { heat_level: 4 }),
110 Some(Contribution { heat_level: 4 }),
111 Some(Contribution { heat_level: 4 }),
112 ]
113 };
114
115 assert_eq!(contribution_week, expected)
116 }
117
118 #[test]
119 fn constructs_partial_contribution_week() {
120 let fragment = Html::parse_fragment(r#"
121 <rect y='60' data-level='1' />
122 <rect y='75' data-level='2' />
123 <rect y='90' data-level='3' />
124 "#);
125
126 let selector = Selector::parse("rect").unwrap();
127 let rects: Vec<_> = fragment.select(&selector).collect();
128 let contribution_week = ContributionWeek::from_days(&rects).unwrap();
129
130 let expected = ContributionWeek {
131 contributions: vec![
132 None,
133 None,
134 None,
135 None,
136 Some(Contribution { heat_level: 1 }),
137 Some(Contribution { heat_level: 2 }),
138 Some(Contribution { heat_level: 3 }),
139 ]
140 };
141
142 assert_eq!(contribution_week, expected)
143 }
144
145 #[test]
146 fn parses_y_attribute() {
147 let fragment = Html::parse_fragment("<rect y='15' data-level='3' />");
148 let selector = Selector::parse("rect").unwrap();
149 let rect_el = fragment.select(&selector).next().unwrap();
150 let y_value = ContributionWeek::parse_y_attr(&rect_el).unwrap();
151
152 assert_eq!(y_value, 15)
153 }
154
155 #[test]
156 fn error_if_no_y_attribute() {
157 let fragment = Html::parse_fragment("<rect data-level='3' />");
158 let selector = Selector::parse("rect").unwrap();
159 let rect_el = fragment.select(&selector).next().unwrap();
160 let y_value = ContributionWeek::parse_y_attr(&rect_el);
161
162 assert_eq!(
163 y_value,
164 Err(HeatmapError::QueryAttribute { attr: Y_ATTR.to_string(), on_alias: "heatmap node".to_string() })
165 )
166 }
167
168 #[test]
169 fn error_if_invalid_y_attribute() {
170 let fragment = Html::parse_fragment("<rect y='fifteen' data-level='three' />");
171 let selector = Selector::parse("rect").unwrap();
172 let rect_el = fragment.select(&selector).next().unwrap();
173 let contribution = ContributionWeek::parse_y_attr(&rect_el);
174
175 assert_eq!(
176 contribution,
177 Err(HeatmapError::ParseAttribute { attr: Y_ATTR.to_string(), on_alias: "heatmap node".to_string() })
178 )
179 }
180
181 #[test]
182 fn gets_first_day_index() {
183 let day_index = ContributionWeek::get_day_index(0).unwrap();
184 assert_eq!(day_index, 0)
185 }
186
187 #[test]
188 fn gets_large_day_index() {
189 let day_index = ContributionWeek::get_day_index(30).unwrap();
190 assert_eq!(day_index, 2)
191 }
192
193 #[test]
194 fn gets_small_day_index() {
195 let day_index = ContributionWeek::get_day_index(26).unwrap();
196 assert_eq!(day_index, 2)
197 }
198
199 #[test]
200 fn error_if_unknown_node_format() {
201 let result = ContributionWeek::get_day_index(20);
202 assert_eq!(result, Err(HeatmapError::UnknownNodeFormat))
203 }
204}