1use crate::error::EPWParseError;
2use chrono::FixedOffset;
3use std::collections::VecDeque;
4use std::fmt;
5use std::io::{BufRead, Lines};
6
7const LOCATION_KEY: &str = "LOCATION";
8const DESIGN_CONDITIONS_KEY: &str = "DESIGN CONDITIONS";
9const TYPICAL_EXTREME_PERIODS_KEY: &str = "TYPICAL/EXTREME PERIODS";
10
11const GROUND_TEMPERATURES_KEY: &str = "GROUND TEMPERATURES";
12const HOLIDAYS_DAYLIGHT_SAVINGS_KEY: &str = "HOLIDAYS/DAYLIGHT SAVINGS";
13const COMMENTS_KEY: &str = "COMMENTS";
14const DATA_PERIODS_KEY: &str = "DATA PERIODS";
15
16#[derive(Debug, PartialEq)]
17pub enum DayOfWeek {
18 Sunday,
19 Monday,
20 Tuesday,
21 Wednesday,
22 Thursday,
23 Friday,
24 Saturday,
25}
26
27#[derive(Debug)]
28pub struct Location {
29 pub city: String,
30 pub state_province_region: String,
31 pub country: String,
32 pub source: String,
33 pub wmo: String,
34 pub latitude: f64,
35 pub longitude: f64,
36 pub time_zone: FixedOffset,
37 pub elevation: f64,
38}
39
40impl fmt::Display for Location {
41 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42 write!(
43 f,
44 "{}°,{}° [{}, {} | {}]",
45 self.latitude, self.longitude, self.city, self.state_province_region, self.country
46 )
47 }
48}
49
50#[derive(Debug, PartialEq)]
51pub struct GroundTemperatureSample {
52 pub depth: f64,
53 pub soil_conductivity: Option<f64>,
54 pub soil_density: Option<f64>,
55 pub soil_specific_heat: Option<f64>,
56 pub january: f64,
57 pub february: f64,
58 pub march: f64,
59 pub april: f64,
60 pub may: f64,
61 pub june: f64,
62 pub july: f64,
63 pub august: f64,
64 pub september: f64,
65 pub october: f64,
66 pub november: f64,
67 pub december: f64,
68}
69
70#[derive(Debug)]
71pub struct Holiday {
72 pub date: String,
73 pub name: String,
74}
75#[derive(Debug)]
76pub struct HolidayDaylightSavings {
77 pub leap_year: bool,
78 pub daylight_savings_start: String,
79 pub daylight_savings_end: String,
80 pub holidays: Vec<Holiday>,
81}
82
83#[derive(Debug)]
84pub struct DataPeriod {
85 pub name: String,
86 pub start_day_of_week: DayOfWeek,
87 pub start_day: String,
88 pub end_day: String,
89}
90
91#[derive(Debug, PartialEq)]
92pub enum PeriodType {
93 Typical,
94 Extreme,
95}
96
97#[derive(Debug)]
98pub struct TypicalExtremePeriod {
99 pub name: String,
100 pub period_type: PeriodType,
101 pub start: String,
102 pub end: String,
103}
104
105#[derive(Debug)]
106pub struct DataPeriods {
107 pub records_per_hour: usize,
108 pub periods: Vec<DataPeriod>,
109}
110
111#[derive(Debug)]
113pub struct Header {
114 pub location: Location,
115 pub design_conditions: Option<Vec<String>>,
116 pub typical_extreme_periods: Vec<TypicalExtremePeriod>,
117 pub ground_temperatures: Vec<GroundTemperatureSample>,
118 pub holidays_daylight_savings: HolidayDaylightSavings,
119 pub comments: Vec<String>,
120 pub data_periods: DataPeriods,
121}
122
123pub fn parse_header<R: BufRead>(lines: &mut Lines<R>) -> Result<Header, EPWParseError> {
124 let mut location: Option<Location> = None;
125 let mut design_conditions: Option<Vec<String>> = None;
126 let mut typical_extreme_periods: Option<Vec<TypicalExtremePeriod>> = None;
127 let mut ground_temperature: Option<Vec<GroundTemperatureSample>> = None;
128 let mut data_periods: Option<DataPeriods> = None;
129 let mut holidays: Option<HolidayDaylightSavings> = None;
130 let mut comments: Vec<String> = Vec::with_capacity(2);
131
132 for line in lines.by_ref().take(8) {
133 let line = line.expect("Unable to read line");
134 if line.starts_with(LOCATION_KEY) {
135 location = match _parse_location(&line) {
136 Ok(val) => Some(val),
137 Err(e) => {
138 return Err(e);
139 }
140 };
141 } else if line.starts_with(GROUND_TEMPERATURES_KEY) {
142 ground_temperature = match _parse_ground_temperature(&line) {
143 Ok(val) => Some(val),
144 Err(e) => {
145 return Err(e);
146 }
147 }
148 } else if line.starts_with(DATA_PERIODS_KEY) {
149 data_periods = match _parse_data_periods(&line) {
150 Ok(val) => Some(val),
151 Err(e) => {
152 return Err(e);
153 }
154 };
155 } else if line.starts_with(TYPICAL_EXTREME_PERIODS_KEY) {
156 typical_extreme_periods = match _parse_typical_extreme_periods(&line) {
157 Ok(val) => Some(val),
158 Err(e) => return Err(e),
159 };
160 } else if line.starts_with(HOLIDAYS_DAYLIGHT_SAVINGS_KEY) {
161 holidays = match _parse_holiday_daylight_savings(&line) {
162 Ok(val) => Some(val),
163 Err(e) => {
164 return Err(e);
165 }
166 }
167 } else if line.starts_with(COMMENTS_KEY) {
168 comments.push(_parse_comment(&line));
169 } else if line.starts_with(DESIGN_CONDITIONS_KEY) {
170 design_conditions = Some(_parse_design_conditions(&line));
171 } else {
172 return Err(EPWParseError::UnexpectedData(format!(
173 "Unexpected Row: {}",
174 line
175 )));
176 }
177 }
178
179 Ok(Header {
180 location: match location {
181 Some(val) => val,
182 None => return Err(EPWParseError::Location("No Location Found".to_string())),
183 },
184 ground_temperatures: match ground_temperature {
185 Some(val) => val,
186 None => {
187 return Err(EPWParseError::GroundTemperature(
188 "No Ground Temperatures Found".to_string(),
189 ))
190 }
191 },
192 holidays_daylight_savings: match holidays {
193 Some(val) => val,
194 None => {
195 return Err(EPWParseError::HolidayDaylightSavings(
196 "No Holidays/Daylight Savings Found".to_string(),
197 ))
198 }
199 },
200 data_periods: match data_periods {
201 Some(val) => val,
202 None => {
203 return Err(EPWParseError::DataPeriods(
204 "No Data Periods Found".to_string(),
205 ))
206 }
207 },
208 typical_extreme_periods: match typical_extreme_periods {
209 Some(val) => val,
210 None => {
211 return Err(EPWParseError::TypicalExtremePeriods(
212 "No Typical/Extreme Periods Found".to_string(),
213 ))
214 }
215 },
216 design_conditions,
217 comments,
218 })
219}
220
221fn _parse_location(line: &str) -> Result<Location, EPWParseError> {
222 if !line.starts_with(LOCATION_KEY) {
223 panic!("_parse_location called with a line that doesn't start with LOCATION");
225 }
226 let parts: Vec<&str> = line.split(",").collect();
227 if parts.len() != 10 {
228 return Err(EPWParseError::Location(format!(
229 "Invalid Location Line: {}",
230 line
231 )));
232 }
233
234 let latitude = match parts[6].parse() {
235 Ok(val) => val,
236 Err(e) => {
237 return Err(EPWParseError::Location(format!(
238 "Invalid Latitude: {} [{}]",
239 parts[6], e
240 )))
241 }
242 };
243
244 let longitude = match parts[7].parse() {
245 Ok(val) => val,
246 Err(e) => {
247 return Err(EPWParseError::Location(format!(
248 "Invalid Longitude: {} [{}]",
249 parts[7], e
250 )))
251 }
252 };
253
254 let time_zone = match FixedOffset::east_opt(parts[8].parse::<f64>().unwrap() as i32 * 3600) {
255 Some(val) => val,
256 None => {
257 return Err(EPWParseError::Location(format!(
258 "Invalid Time Zone: {}",
259 parts[8]
260 )))
261 }
262 };
263
264 let elevation = match parts[9].parse() {
265 Ok(val) => val,
266 Err(e) => {
267 return Err(EPWParseError::Location(format!(
268 "Invalid Elevation: {} [{}]",
269 parts[9], e
270 )))
271 }
272 };
273
274 Ok(Location {
275 city: parts[1].to_string(),
276 state_province_region: parts[2].to_string(),
277 country: parts[3].to_string(),
278 source: parts[4].to_string(),
279 wmo: parts[5].to_string(),
280 latitude,
281 longitude,
282 time_zone,
283 elevation,
284 })
285}
286
287fn _parse_ground_temperature(line: &str) -> Result<Vec<GroundTemperatureSample>, EPWParseError> {
288 if !line.starts_with(GROUND_TEMPERATURES_KEY) {
289 panic!("_parse_ground_temperature called with a line that doesn't start with GROUND TEMPERATURES");
290 }
291
292 let mut parts = line.split(",").collect::<Vec<&str>>();
293 let sample_count: u16 = parts[1].parse().unwrap();
294 let mut samples: Vec<GroundTemperatureSample> = Vec::with_capacity(sample_count as usize);
295 let mut sample_data = parts.split_off(2);
296 for idx in 0..sample_count {
297 if sample_data.len() < 16 {
298 return Err(EPWParseError::GroundTemperature(format!(
299 "Not enough data for sample at index {}: {}",
300 idx,
301 sample_data.join(",")
302 )));
303 }
304
305 let depth = match sample_data[0].parse() {
306 Ok(val) => val,
307 Err(e) => {
308 return Err(EPWParseError::GroundTemperature(format!(
309 "Invalid Depth at index: {} {} [{}]",
310 idx, sample_data[0], e
311 )))
312 }
313 };
314
315 let january = match sample_data[4].parse() {
316 Ok(val) => val,
317 Err(e) => {
318 return Err(EPWParseError::GroundTemperature(format!(
319 "Invalid January temp value at index: {} {} [{}]",
320 idx, sample_data[4], e
321 )))
322 }
323 };
324
325 let february = match sample_data[5].parse() {
326 Ok(val) => val,
327 Err(e) => {
328 return Err(EPWParseError::GroundTemperature(format!(
329 "Invalid February temp value at index: {} {} [{}]",
330 idx, sample_data[5], e
331 )))
332 }
333 };
334
335 let march = match sample_data[6].parse() {
336 Ok(val) => val,
337 Err(e) => {
338 return Err(EPWParseError::GroundTemperature(format!(
339 "Invalid March temp value at index: {} {} [{}]",
340 idx, sample_data[6], e
341 )))
342 }
343 };
344
345 let april = match sample_data[7].parse() {
346 Ok(val) => val,
347 Err(e) => {
348 return Err(EPWParseError::GroundTemperature(format!(
349 "Invalid April temp value at index: {} {} [{}]",
350 idx, sample_data[7], e
351 )))
352 }
353 };
354
355 let may_value = match sample_data[8].parse() {
356 Ok(val) => val,
357 Err(e) => {
358 return Err(EPWParseError::GroundTemperature(format!(
359 "Invalid May temp value at index: {} {} [{}]",
360 idx, sample_data[8], e
361 )))
362 }
363 };
364
365 let june = match sample_data[9].parse() {
366 Ok(val) => val,
367 Err(e) => {
368 return Err(EPWParseError::GroundTemperature(format!(
369 "Invalid June temp value at index: {} {} [{}]",
370 idx, sample_data[9], e
371 )))
372 }
373 };
374
375 let july = match sample_data[10].parse() {
376 Ok(val) => val,
377 Err(e) => {
378 return Err(EPWParseError::GroundTemperature(format!(
379 "Invalid July temp value at index: {} {} [{}]",
380 idx, sample_data[10], e
381 )))
382 }
383 };
384
385 let august = match sample_data[11].parse() {
386 Ok(val) => val,
387 Err(e) => {
388 return Err(EPWParseError::GroundTemperature(format!(
389 "Invalid August temp value at index: {} {} [{}]",
390 idx, sample_data[11], e
391 )))
392 }
393 };
394
395 let september = match sample_data[12].parse() {
396 Ok(val) => val,
397 Err(e) => {
398 return Err(EPWParseError::GroundTemperature(format!(
399 "Invalid September temp value at index: {} {} [{}]",
400 idx, sample_data[12], e
401 )))
402 }
403 };
404
405 let october = match sample_data[13].parse() {
406 Ok(val) => val,
407 Err(e) => {
408 return Err(EPWParseError::GroundTemperature(format!(
409 "Invalid October temp value at index: {} {} [{}]",
410 idx, sample_data[13], e
411 )))
412 }
413 };
414
415 let november = match sample_data[14].parse() {
416 Ok(val) => val,
417 Err(e) => {
418 return Err(EPWParseError::GroundTemperature(format!(
419 "Invalid November temp value at index: {} {} [{}]",
420 idx, sample_data[14], e
421 )))
422 }
423 };
424
425 let december = match sample_data[15].parse() {
426 Ok(val) => val,
427 Err(e) => {
428 return Err(EPWParseError::GroundTemperature(format!(
429 "Invalid December temp value at index: {} {} [{}]",
430 idx, sample_data[15], e
431 )))
432 }
433 };
434
435 let sample = GroundTemperatureSample {
436 depth,
437 soil_conductivity: sample_data[1].parse().ok(),
438 soil_density: sample_data[2].parse().ok(),
439 soil_specific_heat: sample_data[3].parse().ok(),
440 january,
441 february,
442 march,
443 april,
444 may: may_value,
445 june,
446 july,
447 august,
448 september,
449 october,
450 november,
451 december,
452 };
453 samples.push(sample);
454 sample_data = sample_data.split_off(16)
455 }
456 Ok(samples)
457}
458
459fn _parse_comment(line: &str) -> String {
460 if !line.starts_with(COMMENTS_KEY) {
461 panic!(
462 "_parse_comment called with a line that doesn't start with {}",
463 COMMENTS_KEY
464 );
465 }
466 line.splitn(2, ",").collect::<Vec<&str>>()[1].to_string()
467}
468fn _parse_data_periods(line: &str) -> Result<DataPeriods, EPWParseError> {
469 if !line.starts_with(DATA_PERIODS_KEY) {
470 panic!(
471 "_parse_data_periods called with a line that doesn't start with {}",
472 DATA_PERIODS_KEY
473 );
474 }
475
476 let mut parts = line.split(",").collect::<Vec<&str>>();
477
478 let period_count = match parts[1].parse() {
479 Ok(val) => val,
480 Err(e) => {
481 return Err(EPWParseError::DataPeriods(format!(
482 "Invalid period count: {} [{}]",
483 parts[1], e
484 )))
485 }
486 };
487
488 let records_per_hour = match parts[2].parse() {
489 Ok(val) => val,
490 Err(e) => {
491 return Err(EPWParseError::DataPeriods(format!(
492 "Invalid records per hour: {} [{}]",
493 parts[2], e
494 )))
495 }
496 };
497 let mut periods: Vec<DataPeriod> = Vec::with_capacity(period_count);
498 let mut period_data = parts.split_off(3);
499 for idx in 0..period_count {
500 if period_data.len() < 4 {
501 return Err(EPWParseError::DataPeriods(format!(
502 "Not enough data for period at index {}: {}",
503 idx,
504 period_data.join(",")
505 )));
506 }
507
508 let start_day_of_week = match period_data[1] {
509 "Sunday" => DayOfWeek::Sunday,
510 "Monday" => DayOfWeek::Monday,
511 "Tuesday" => DayOfWeek::Tuesday,
512 "Wednesday" => DayOfWeek::Wednesday,
513 "Thursday" => DayOfWeek::Thursday,
514 "Friday" => DayOfWeek::Friday,
515 "Saturday" => DayOfWeek::Saturday,
516 e => {
517 return Err(EPWParseError::DataPeriods(format!(
518 "Invalid day of week at index {}: {} [{}]",
519 idx, period_data[1], e
520 )))
521 }
522 };
523
524 let period = DataPeriod {
525 name: period_data[0].to_string(),
526 start_day_of_week,
527 start_day: period_data[2].to_string(),
528 end_day: period_data[3].to_string(),
529 };
530 periods.push(period);
531 period_data = period_data.split_off(4)
532 }
533 Ok(DataPeriods {
534 records_per_hour,
535 periods,
536 })
537}
538
539fn _parse_typical_extreme_periods(line: &str) -> Result<Vec<TypicalExtremePeriod>, EPWParseError> {
540 if !line.starts_with(TYPICAL_EXTREME_PERIODS_KEY) {
541 panic!(
542 "_parse_typical_extreme_periods called with a line that doesn't start with {}",
543 TYPICAL_EXTREME_PERIODS_KEY
544 );
545 }
546
547 let mut parts = line.split(",").collect::<Vec<&str>>();
548
549 let period_count = match parts[1].parse() {
550 Ok(val) => val,
551 Err(e) => {
552 return Err(EPWParseError::TypicalExtremePeriods(format!(
553 "Invalid period count: {} [{}]",
554 parts[1], e
555 )))
556 }
557 };
558
559 let mut periods: Vec<TypicalExtremePeriod> = Vec::with_capacity(period_count);
560 let mut period_data = parts.split_off(2);
561 for idx in 0..period_count {
562 if period_data.len() < 4 {
563 return Err(EPWParseError::TypicalExtremePeriods(format!(
564 "Not enough data for period at index {}: {}",
565 idx,
566 period_data.join(",")
567 )));
568 }
569
570 let name = period_data[0].to_string();
571 let period_type = match period_data[1] {
572 "Typical" => PeriodType::Typical,
573 "Extreme" => PeriodType::Extreme,
574 _ => {
575 return Err(EPWParseError::TypicalExtremePeriods(format!(
576 "Invalid period type at index {}: {}",
577 idx, period_data[1]
578 )))
579 }
580 };
581 let start = period_data[2].to_string();
582 let end = period_data[3].to_string();
583
584 let period = TypicalExtremePeriod {
585 name,
586 period_type,
587 start,
588 end,
589 };
590 periods.push(period);
591 period_data = period_data.split_off(4)
592 }
593 Ok(periods)
594}
595
596fn _parse_holiday_daylight_savings(line: &str) -> Result<HolidayDaylightSavings, EPWParseError> {
597 if !line.starts_with(HOLIDAYS_DAYLIGHT_SAVINGS_KEY) {
598 panic!(
599 "_parse_holidays_daylight_savings called with a line that doesn't start with '{}'",
600 HOLIDAYS_DAYLIGHT_SAVINGS_KEY
601 );
602 }
603
604 let mut parts = line.split(",").collect::<Vec<&str>>();
605
606 let leap_year = match parts[1] {
607 "Yes" => true,
608 "No" => false,
609 _ => {
610 return Err(EPWParseError::HolidayDaylightSavings(format!(
611 "Invalid Leap Year Value: {}",
612 parts[1]
613 )))
614 }
615 };
616
617 let daylight_savings_start = match parts[2].parse() {
618 Ok(val) => val,
619 Err(e) => {
620 return Err(EPWParseError::HolidayDaylightSavings(format!(
621 "Invalid Daylight Savings Start Day: {} [{}]",
622 parts[2], e
623 )))
624 }
625 };
626
627 let daylight_savings_end = match parts[3].parse() {
628 Ok(val) => val,
629 Err(e) => {
630 return Err(EPWParseError::HolidayDaylightSavings(format!(
631 "Invalid Daylight Savings End Day: {} [{}]",
632 parts[3], e
633 )))
634 }
635 };
636
637 let holiday_count = match parts[4].parse() {
638 Ok(val) => val,
639 Err(e) => {
640 return Err(EPWParseError::HolidayDaylightSavings(format!(
641 "Invalid holiday count: {} [{}]",
642 parts[4], e
643 )))
644 }
645 };
646
647 let mut holidays: Vec<Holiday> = Vec::with_capacity(holiday_count);
648 let mut holiday_data = parts.split_off(4);
649 for idx in 0..holiday_count {
650 if holiday_data.len() < 2 {
651 return Err(EPWParseError::HolidayDaylightSavings(format!(
652 "Not enough data for holiday at index {}: {}",
653 idx,
654 holiday_data.join(",")
655 )));
656 }
657
658 holidays.push(Holiday {
659 name: holiday_data[0].to_string(),
660 date: holiday_data[1].to_string(),
661 });
662 holiday_data = holiday_data.split_off(2);
663 }
664
665 Ok(HolidayDaylightSavings {
666 leap_year,
667 daylight_savings_start,
668 daylight_savings_end,
669 holidays,
670 })
671}
672
673fn _parse_design_conditions(line: &str) -> Vec<String> {
674 if !line.starts_with(DESIGN_CONDITIONS_KEY) {
675 panic!(
676 "_parse_design_conditions called with a line that doesn't start with '{}'",
677 DESIGN_CONDITIONS_KEY
678 );
679 }
680
681 let mut parts: VecDeque<&str> = line.split(",").collect();
682 parts.pop_front();
683 parts.into_iter().map(String::from).collect()
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use std::fs::File;
690 use std::io::BufReader;
691
692 const TEST_FILE: &str = "./data/USA_FL_Tampa_TMY2.epw";
693
694 fn _read_test_file() -> Lines<BufReader<File>> {
695 let file = File::open(TEST_FILE).unwrap();
696 let reader = BufReader::new(file);
697 reader.lines()
698 }
699
700 #[test]
701 fn test_parse_location_from_file() {
702 let mut lines = _read_test_file();
703 let header = parse_header(&mut lines);
704
705 assert!(header.is_ok());
706 let header = header.unwrap();
707 let location = header.location;
708
709 assert_eq!(location.city, "TAMPA");
710 assert_eq!(location.state_province_region, "FL");
711 assert_eq!(location.country, "USA");
712 assert_eq!(location.source, "TMY2-12842");
713 assert_eq!(location.wmo, "722110");
714 assert_eq!(location.latitude, 27.97);
715 assert_eq!(location.longitude, -82.53);
716 assert_eq!(
717 location.time_zone,
718 FixedOffset::east_opt(-5 * 3600).unwrap()
719 );
720 }
721
722 #[test]
723 fn test_parse_typical_extreme_periods_from_file() {
724 let mut lines = _read_test_file();
725 let header = parse_header(&mut lines);
726
727 assert!(header.is_ok());
728 let header = header.unwrap();
729 let periods = header.typical_extreme_periods;
730 assert_eq!(6, periods.len());
731
732 assert_eq!(
733 "Summer - Week Nearest Max Temperature For Period",
734 periods[0].name
735 );
736 assert_eq!(PeriodType::Extreme, periods[0].period_type);
737 assert_eq!("7/ 6", periods[0].start);
738 assert_eq!("7/12", periods[0].end);
739
740 assert_eq!(
741 "Summer - Week Nearest Average Temperature For Period",
742 periods[1].name
743 );
744 assert_eq!(PeriodType::Typical, periods[1].period_type);
745 assert_eq!("8/ 3", periods[1].start);
746 assert_eq!("8/ 9", periods[1].end);
747
748 assert_eq!(
749 "Winter - Week Nearest Min Temperature For Period",
750 periods[2].name
751 );
752 assert_eq!(PeriodType::Extreme, periods[2].period_type);
753 assert_eq!("2/10", periods[2].start);
754 assert_eq!("2/16", periods[2].end);
755
756 assert_eq!(
757 "Winter - Week Nearest Average Temperature For Period",
758 periods[3].name
759 );
760 assert_eq!(PeriodType::Typical, periods[3].period_type);
761 assert_eq!("12/22", periods[3].start);
762 assert_eq!("1/ 5", periods[3].end);
763
764 assert_eq!(
765 "Autumn - Week Nearest Average Temperature For Period",
766 periods[4].name
767 );
768 assert_eq!(PeriodType::Typical, periods[4].period_type);
769 assert_eq!("10/20", periods[4].start);
770 assert_eq!("10/26", periods[4].end);
771
772 assert_eq!(
773 "Spring - Week Nearest Average Temperature For Period",
774 periods[5].name
775 );
776 assert_eq!(PeriodType::Typical, periods[5].period_type);
777 assert_eq!("4/19", periods[5].start);
778 assert_eq!("4/25", periods[5].end);
779 }
780
781 #[test]
782 fn test_parse_ground_temperature_from_file() {
783 let mut lines = _read_test_file();
784 let header = parse_header(&mut lines);
785
786 assert!(header.is_ok());
787 let header = header.unwrap();
788 let temperatures = header.ground_temperatures;
789
790 assert_eq!(3, temperatures.len());
791
792 assert_eq!(
793 GroundTemperatureSample {
794 depth: 0.5,
795 soil_conductivity: None,
796 soil_density: None,
797 soil_specific_heat: None,
798 january: 16.22,
799 february: 17.29,
800 march: 19.37,
801 april: 21.34,
802 may: 25.08,
803 june: 27.04,
804 july: 27.58,
805 august: 26.59,
806 september: 24.28,
807 october: 21.42,
808 november: 18.59,
809 december: 16.72,
810 },
811 temperatures[0]
812 );
813
814 assert_eq!(
815 GroundTemperatureSample {
816 depth: 2.0,
817 soil_conductivity: None,
818 soil_density: None,
819 soil_specific_heat: None,
820 january: 17.69,
821 february: 17.95,
822 march: 19.14,
823 april: 20.46,
824 may: 23.33,
825 june: 25.17,
826 july: 26.08,
827 august: 25.87,
828 september: 24.56,
829 october: 22.58,
830 november: 20.35,
831 december: 18.60,
832 },
833 temperatures[1]
834 );
835
836 assert_eq!(
837 GroundTemperatureSample {
838 depth: 4.0,
839 soil_conductivity: None,
840 soil_density: None,
841 soil_specific_heat: None,
842 january: 19.22,
843 february: 19.03,
844 march: 19.55,
845 april: 20.3,
846 may: 22.2,
847 june: 23.62,
848 july: 24.54,
849 august: 24.77,
850 september: 24.19,
851 october: 23.02,
852 november: 21.51,
853 december: 20.15,
854 },
855 temperatures[2]
856 );
857 }
858}