epw_rs/
header.rs

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/// EPW File header
112#[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        // This should never happen
224        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}