use std::io::BufRead;
use chrono::{FixedOffset, NaiveDateTime};
use lazy_static::lazy_static;
use regex::Regex;
use crate::{
AnalogChannel, AnalogScalingMode, Comtrade, ComtradeBuilder, DataFormat, FileType,
FormatRevision, LeapSecondStatus, SamplingRate, StatusChannel, TimeQuality,
};
const CFG_SEPARATOR: &'static str = ",";
const CFG_DATETIME_FORMAT: &'static str = "%d/%m/%Y,%H:%M:%S%.f";
pub type ParseResult<T> = std::result::Result<T, ParseError>;
#[derive(Debug, Clone)]
pub struct ParseError {
message: String,
}
impl ParseError {
fn new(message: String) -> Self {
ParseError { message }
}
}
impl FileType {
fn from_str(value: String) -> ParseResult<Self> {
match value.trim().to_lowercase().as_str() {
"cfg" => Ok(FileType::Cfg),
"dat" => Ok(FileType::Dat),
"hdr" => Ok(FileType::Hdr),
"inf" => Ok(FileType::Inf),
_ => Err(ParseError::new(format!("invalid file type: '{}'", value))),
}
}
}
impl Default for FormatRevision {
fn default() -> Self {
FormatRevision::Revision1991
}
}
impl FormatRevision {
fn from_str(value: &str) -> ParseResult<Self> {
match value {
"1991" => Ok(FormatRevision::Revision1991),
"1999" => Ok(FormatRevision::Revision1999),
"2013" => Ok(FormatRevision::Revision2013),
_ => Err(ParseError::new(format!(
"unrecognised or invalid COMTRADE format revision: '{}'",
value.to_owned(),
))),
}
}
}
impl DataFormat {
fn from_str(value: &str) -> ParseResult<Self> {
match value.trim().to_lowercase().as_str() {
"ascii" => Ok(DataFormat::Ascii),
"binary" => Ok(DataFormat::Binary16),
"binary32" => Ok(DataFormat::Binary32),
"float32" => Ok(DataFormat::Float32),
_ => Err(ParseError::new(format!(
"unrecognised or invalid COMTRADE data format: '{}'",
value.to_owned(),
))),
}
}
}
impl AnalogScalingMode {
fn from_str(value: &str) -> ParseResult<Self> {
match value.to_lowercase().as_str() {
"p" => Ok(AnalogScalingMode::Primary),
"s" => Ok(AnalogScalingMode::Secondary),
_ => Err(ParseError::new(format!(
"invalid analog scaling mode: '{}'; must be one of: 's', 'S', 'p', 'P'",
value,
))),
}
}
}
impl TimeQuality {
fn parse(value: &str) -> ParseResult<Self> {
match value.to_lowercase().trim() {
"f" => Ok(TimeQuality::ClockFailure),
"b" => Ok(TimeQuality::ClockUnlocked(1)),
"a" => Ok(TimeQuality::ClockUnlocked(0)),
"9" => Ok(TimeQuality::ClockUnlocked(-1)),
"8" => Ok(TimeQuality::ClockUnlocked(-2)),
"7" => Ok(TimeQuality::ClockUnlocked(-3)),
"6" => Ok(TimeQuality::ClockUnlocked(-4)),
"5" => Ok(TimeQuality::ClockUnlocked(-5)),
"4" => Ok(TimeQuality::ClockUnlocked(-6)),
"3" => Ok(TimeQuality::ClockUnlocked(-7)),
"2" => Ok(TimeQuality::ClockUnlocked(-8)),
"1" => Ok(TimeQuality::ClockUnlocked(-9)),
"0" => Ok(TimeQuality::ClockLocked),
_ => Err(ParseError::new(format!(
"invalid time quality code '{}'",
value,
))),
}
}
}
impl LeapSecondStatus {
fn parse(value: &str) -> ParseResult<Self> {
match value.trim() {
"3" => Ok(LeapSecondStatus::NoCapability),
"2" => Ok(LeapSecondStatus::Subtracted),
"1" => Ok(LeapSecondStatus::Added),
"0" => Ok(LeapSecondStatus::NotPresent),
_ => Err(ParseError::new(format!(
"invalid leap second indicator '{}'",
value,
))),
}
}
}
lazy_static! {
static ref CFF_HEADER_REGEXP: Regex = Regex::new(r#"(?i)---\s*file type:\s*(?P<file_type>[a-z]+)(\s+(?P<data_format>[a-z]+))?\s*(:\s*(?P<data_size>\d+))?\s*---$"#).unwrap();
static ref DATE_REGEXP: Regex = Regex::new("([0-9]{1,2})/([0-9]{1,2})/([0-9]{2,4})").unwrap();
static ref TIME_REGEXP: Regex = Regex::new("([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.([0-9]{1,12}))?").unwrap();
}
pub struct ComtradeParserBuilder<T: BufRead> {
cff_file: Option<T>,
cfg_file: Option<T>,
dat_file: Option<T>,
hdr_file: Option<T>,
inf_file: Option<T>,
}
impl<T: BufRead> ComtradeParserBuilder<T> {
pub fn new() -> Self {
Self {
cff_file: None,
cfg_file: None,
dat_file: None,
hdr_file: None,
inf_file: None,
}
}
pub fn cff_file(mut self, file: T) -> Self {
self.cff_file = Some(file);
self
}
pub fn cfg_file(mut self, file: T) -> Self {
self.cfg_file = Some(file);
self
}
pub fn dat_file(mut self, file: T) -> Self {
self.dat_file = Some(file);
self
}
pub fn hdr_file(mut self, file: T) -> Self {
self.hdr_file = Some(file);
self
}
pub fn inf_file(mut self, file: T) -> Self {
self.inf_file = Some(file);
self
}
pub fn build(self) -> ComtradeParser<T> {
ComtradeParser::new(
self.cff_file,
self.cfg_file,
self.dat_file,
self.hdr_file,
self.inf_file,
)
}
}
pub struct ComtradeParser<T: BufRead> {
cff_file: Option<T>,
cfg_file: Option<T>,
dat_file: Option<T>,
hdr_file: Option<T>,
inf_file: Option<T>,
cfg_contents: String,
ascii_dat_contents: String,
binary_dat_contents: Vec<u8>,
hdr_contents: String,
inf_contents: String,
builder: ComtradeBuilder,
num_analog_channels: u32,
num_status_channels: u32,
data_format: Option<DataFormat>,
}
impl<T: BufRead> ComtradeParser<T> {
pub fn new(
cff_file: Option<T>,
cfg_file: Option<T>,
dat_file: Option<T>,
hdr_file: Option<T>,
inf_file: Option<T>,
) -> Self {
Self {
cff_file,
cfg_file,
dat_file,
hdr_file,
inf_file,
cfg_contents: String::new(),
ascii_dat_contents: String::new(),
binary_dat_contents: vec![],
hdr_contents: String::new(),
inf_contents: String::new(),
builder: ComtradeBuilder::default(),
num_analog_channels: 0,
num_status_channels: 0,
data_format: None,
}
}
pub fn dat_file(mut self, file: T) -> Self {
self.dat_file = Some(file);
self
}
pub fn hdr_file(mut self, file: T) -> Self {
self.hdr_file = Some(file);
self
}
pub fn inf_file(mut self, file: T) -> Self {
self.inf_file = Some(file);
self
}
pub fn parse(mut self) -> ParseResult<Comtrade> {
if self.cff_file.is_some() {
self.load_cff()?;
self.parse_cfg()?;
self.parse_dat()?;
} else {
if let Some(ref mut cfg_file) = self.cfg_file {
cfg_file
.read_to_string(&mut self.cfg_contents)
.or(Err(ParseError::new(
"unable to read specified .cfg file".to_string(),
)))?;
} else {
return Err(ParseError::new(
"you must specify either .cff or .cfg file".to_string(),
));
}
self.parse_cfg()?;
if let Some(ref mut dat_file) = self.dat_file {
match self.data_format {
Some(DataFormat::Ascii) => {
dat_file
.read_to_string(&mut self.ascii_dat_contents)
.or(Err(ParseError::new(
"unable to read specified .dat file".into(),
)))?;
}
None => {
return Err(ParseError::new("unknown data format for data file.".into()));
}
_ => {
dat_file.read_to_end(&mut self.binary_dat_contents).or(Err(
ParseError::new("unable to read specified .dat file".into()),
))?;
}
}
} else {
return Err(ParseError::new(
"you must specify either .cff or .dat file".to_string(),
));
}
self.parse_dat()?;
if let Some(ref mut hdr_file) = self.hdr_file {
hdr_file
.read_to_string(&mut self.hdr_contents)
.or(Err(ParseError::new(
"unable to read specified .hdr file".to_string(),
)))?;
}
if let Some(ref mut inf_file) = self.inf_file {
inf_file
.read_to_string(&mut self.inf_contents)
.or(Err(ParseError::new(
"unable to read specified .inf file".to_string(),
)))?;
}
}
Ok(self.builder.build().unwrap())
}
fn load_cff(&mut self) -> ParseResult<()> {
let file = match &mut self.cff_file {
Some(reader) => reader,
None => {
return Err(ParseError::new(
"tried to parse .cff file, but file not specified".to_string(),
))
}
};
let mut cfg_lines: Vec<String> = vec![];
let mut dat_lines: Vec<String> = vec![];
let mut hdr_lines: Vec<String> = vec![];
let mut inf_lines: Vec<String> = vec![];
let mut current_file: Option<FileType> = None;
let mut data_format: Option<DataFormat> = None;
let mut data_size: Option<usize> = None;
loop {
let mut line = String::new();
let bytes_read = file.read_line(&mut line).unwrap();
if bytes_read == 0 {
break;
}
line = line.trim().to_string();
let maybe_file_header_match = CFF_HEADER_REGEXP.captures(line.as_str());
if let Some(header_match) = maybe_file_header_match {
let file_type_token = header_match.name("file_type").ok_or(ParseError::new(
"unable to find file type in CFF header Regexp".to_string(),
))?;
let maybe_data_format_token = header_match.name("data_format");
let maybe_data_size_token = header_match.name("data_size");
current_file = Some(FileType::from_str(file_type_token.as_str().to_string())?);
if let Some(data_format_token) = maybe_data_format_token {
data_format = Some(DataFormat::from_str(data_format_token.as_str())?);
}
if let Some(data_size_token) = maybe_data_size_token {
data_size = Some(data_size_token.as_str().to_string().parse::<usize>().or(
Err(ParseError::new(format!(
"unable to parse .dat size: '{}'",
data_size_token.as_str()
))),
)?)
}
continue;
}
match current_file {
Some(FileType::Cfg) => cfg_lines.push(line),
Some(FileType::Dat) => {
if data_format == Some(DataFormat::Ascii) {
dat_lines.push(line);
} else {
unimplemented!()
}
}
Some(FileType::Hdr) => hdr_lines.push(line),
Some(FileType::Inf) => inf_lines.push(line),
None => {
return Err(ParseError::new(
"encountered file contents line before header in .cff".to_string(),
))
}
}
}
self.cfg_contents = cfg_lines.join("\n");
self.ascii_dat_contents = dat_lines.join("\n");
self.hdr_contents = hdr_lines.join("\n");
self.inf_contents = inf_lines.join("\n");
Ok(())
}
fn parse_cfg(&mut self) -> ParseResult<()> {
let mut lines = self.cfg_contents.split("\n");
let early_end_err = ParseError::new("unexpected end of .cfg file".to_string());
let mut line_number = 1;
let mut line = "";
let mut line_values: Vec<&str> = vec![];
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
self.builder.station_name(line_values[0].to_string());
self.builder.recording_device_id(line_values[1].to_string());
let format_revision = match line_values.len() {
3 => FormatRevision::from_str(line_values[2].trim())?,
2 => FormatRevision::Revision1991,
_ => {
return Err(ParseError::new(format!(
"unexpected number of values on line {}",
line_number
)))
}
};
self.builder.revision(format_revision);
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
if line_values.len() != 3 {
return Err(ParseError::new(format!(
"unexpected number of values on line {}",
line_number
)));
}
let num_total_channels =
line_values[0]
.trim()
.to_string()
.parse()
.or(Err(ParseError::new(format!(
"invalid integer value for number of total channels: '{}'",
line_values[0]
))))?;
self.builder.num_total_channels(num_total_channels);
let mut num_analog_channels_token = line_values[1].to_string();
num_analog_channels_token.pop();
let num_analog_channels = num_analog_channels_token
.trim()
.to_string()
.parse()
.or(Err(ParseError::new(format!(
"invalid integer value for number of analog channels: '{}'",
num_analog_channels_token
))))?;
self.builder.num_analog_channels(num_analog_channels);
self.num_analog_channels = num_analog_channels;
let mut num_status_channels_token = line_values[2].to_string();
num_status_channels_token.pop();
let num_status_channels = num_status_channels_token
.trim()
.to_string()
.parse()
.or(Err(ParseError::new(format!(
"invalid integer value for number of status channels: '{}'",
num_status_channels_token
))))?;
self.builder.num_status_channels(num_status_channels);
self.num_status_channels = num_status_channels;
line_number += 1;
let mut analog_channels: Vec<AnalogChannel> =
Vec::with_capacity(self.num_analog_channels as usize);
let mut status_channels: Vec<StatusChannel> =
Vec::with_capacity(self.num_status_channels as usize);
for i in 0..self.num_analog_channels {
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
if line_values.len() != 13 {
return Err(ParseError::new(format!(
"unexpected number of values on line {}",
line_number
)));
}
let analog_index =
line_values[0]
.trim()
.to_string()
.parse::<u32>()
.or(Err(ParseError::new(format!(
"invalid integer value for analog channel {} index: {}",
i, line_values[0]
))))?;
let name = line_values[1].to_string();
let phase = line_values[2].to_string(); let circuit_component_being_monitored = line_values[3].to_string(); let units = line_values[4].to_string();
let multiplier =
line_values[5]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} multiplier: {}",
i, line_values[5]
))))?;
let offset_adder =
line_values[6]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} offset adder: {}",
i, line_values[6]
))))?;
let skew =
line_values[7]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} skew: {}",
i, line_values[7]
))))?;
let min_value =
line_values[8]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} minimum value: {}",
i, line_values[8]
))))?;
let max_value =
line_values[9]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} maximum value: {}",
i, line_values[9]
))))?;
let primary_factor =
line_values[10]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} primary factor: {}",
i, line_values[10]
))))?;
let secondary_factor =
line_values[11]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for analog channel {} secondary factor: {}",
i, line_values[11]
))))?;
let scaling_mode = AnalogScalingMode::from_str(line_values[12].trim())?;
analog_channels.push(AnalogChannel {
index: analog_index,
name,
phase,
circuit_component_being_monitored,
units,
min_value,
max_value,
multiplier,
offset_adder,
skew,
primary_factor,
secondary_factor,
scaling_mode,
data: vec![],
});
line_number += 1;
}
self.builder.analog_channels(analog_channels);
for i in 0..self.num_status_channels {
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
if line_values.len() != 5 {
return Err(ParseError::new(format!(
"unexpected number of values on line {}",
line_number
)));
}
let status_index =
line_values[0]
.trim()
.to_string()
.parse::<u32>()
.or(Err(ParseError::new(format!(
"invalid integer value for status channel {} index: {}",
i, line_values[0]
))))?;
let name = line_values[1].to_string();
let phase = line_values[2].to_string(); let circuit_component_being_monitored = line_values[3].to_string();
let normal_status_value =
line_values[4]
.trim()
.to_string()
.parse::<u8>()
.or(Err(ParseError::new(format!(
"invalid integer value for status channel {} normal value: {}",
i, line_values[4]
))))?;
if normal_status_value != 0 && normal_status_value != 1 {
return Err(ParseError::new(format!("invalid normal status value for status channel {}: {}; expected one of : '0', '1'", i, line_values[4])));
}
status_channels.push(StatusChannel {
index: status_index,
name,
phase,
circuit_component_being_monitored,
normal_status_value,
data: vec![],
});
line_number += 1;
}
self.builder.status_channels(status_channels);
line = lines.next().ok_or(early_end_err.clone())?;
let line_frequency = line
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid real numeric value for line frequency: '{}'",
line,
))))?;
self.builder.line_frequency(line_frequency);
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
if line_values.len() != 1 {
return Err(ParseError::new(format!(
"unexpected number of values on line {}",
line_number
)));
}
let num_sampling_rates =
line_values[0]
.trim()
.to_string()
.parse::<u32>()
.or(Err(ParseError::new(format!(
"invalid integer value for number of sample rates: {}",
line_values[0]
))))?;
let mut sampling_rates: Vec<SamplingRate> = Vec::with_capacity(num_sampling_rates as usize);
for i in 0..num_sampling_rates {
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
if line_values.len() != 2 {
return Err(ParseError::new(format!(
"unexpected number of values on line {}",
line_number
)));
}
let rate_hz =
line_values[0]
.trim()
.to_string()
.parse::<f64>()
.or(Err(ParseError::new(format!(
"invalid float value for sample rate frequency for rate n# {} on line {}: {}",
i, line_number, line_values[0]
))))?;
let end_sample_number =
line_values[1]
.trim()
.to_string()
.parse::<u32>()
.or(Err(ParseError::new(format!(
"invalid integer value for end sample number for rate n# {} on line {}: {}",
i, line_number, line_values[1]
))))?;
sampling_rates.push(SamplingRate {
rate_hz,
end_sample_number,
});
}
if num_sampling_rates == 0 {
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
}
self.builder.sampling_rates(sampling_rates);
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
let start_time = NaiveDateTime::parse_from_str(line.trim(), CFG_DATETIME_FORMAT).or(
Err(ParseError::new(format!(
"invalid datetime value for start time on line {}: {}",
line_number, line,
))),
)?;
self.builder.start_time(start_time);
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
let trigger_time = NaiveDateTime::parse_from_str(line.trim(), CFG_DATETIME_FORMAT).or(
Err(ParseError::new(format!(
"invalid datetime value for trigger time on line {}: {}",
line_number, line,
))),
)?;
self.builder.trigger_time(trigger_time);
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
let data_format = DataFormat::from_str(line)?;
self.data_format = Some(data_format.clone());
self.builder.data_format(data_format);
if format_revision == FormatRevision::Revision1991 {
return Ok(());
}
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
let time_mult = line.trim().parse::<f64>().or(Err(ParseError::new(format!(
"invalid float value for time multiplication factor on line {}: {}",
line_number, line,
))))?;
self.builder.timestamp_multiplication_factor(time_mult);
self.builder.time_offset(None);
self.builder.local_offset(None);
self.builder.time_quality(None);
self.builder.leap_second_status(None);
if format_revision == FormatRevision::Revision1999 {
return Ok(());
}
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
self.builder.time_offset(parse_time_offset(line_values[0])?);
self.builder
.local_offset(parse_time_offset(line_values[1])?);
line_number += 1;
line = lines.next().ok_or(early_end_err.clone())?;
line_values = line.split(CFG_SEPARATOR).collect();
let tmq_code = TimeQuality::parse(line_values[0])?;
self.builder.time_quality(Some(tmq_code));
let leap_second_status = LeapSecondStatus::parse(line_values[1])?;
self.builder.leap_second_status(Some(leap_second_status));
Ok(())
}
fn parse_dat(&mut self) -> ParseResult<()> {
match self.data_format {
Some(DataFormat::Ascii) => self.parse_dat_ascii(),
Some(DataFormat::Binary16) => self.parse_dat_binary16(),
Some(DataFormat::Binary32) => self.parse_dat_binary32(),
Some(DataFormat::Float32) => self.parse_dat_float32(),
None => Err(ParseError::new("Data format not specified.".into())),
}
}
fn parse_dat_ascii(&mut self) -> ParseResult<()> {
let mut analog_channels = &mut self.builder.analog_channels.as_mut().unwrap();
let mut status_channels = &mut self.builder.status_channels.as_mut().unwrap();
let expected_num_cols = (self.num_status_channels + self.num_analog_channels + 2) as usize;
let mut sample_numbers: Vec<u32> = Vec::with_capacity(0);
let mut timestamps: Vec<Option<u32>> = Vec::with_capacity(0);
for (i, line) in self
.ascii_dat_contents
.split("\n")
.filter(|l| !l.trim().is_empty())
.enumerate()
{
let data_values: Vec<&str> = line.split(",").collect();
if data_values.len() != expected_num_cols {
return Err(ParseError::new(format!(
"Row {} has incorrect number of columns; expected {} but got {}.",
i,
expected_num_cols,
data_values.len()
)));
}
let sample_number = data_values[0]
.trim()
.parse::<u32>()
.or(Err(ParseError::new(format!(
"[DAT] Invalid sample number {} on line {}",
data_values[0].trim(),
i + 1
))))?;
sample_numbers.push(sample_number);
let timestamp = match data_values[1].trim() {
"" => None, v => Some(v.parse::<u32>().or(Err(ParseError::new(format!(
"[DAT] Invalid timestamp {} on line {}.",
data_values[1].trim(),
i
))))?),
};
timestamps.push(timestamp);
for channel_idx in 0..self.num_analog_channels {
let raw_value = data_values[(channel_idx + 2) as usize].trim();
let datum = raw_value.parse::<f64>().or(Err(ParseError::new(format!(
"[DAT] Invalid float value {} in analog channel {} on line {}.",
raw_value,
channel_idx + 1,
i + 1
))))?;
analog_channels[channel_idx as usize].push_datum(datum);
}
for channel_idx in 0..self.num_status_channels {
let raw_value =
data_values[(channel_idx + self.num_analog_channels + 2) as usize].trim();
let datum = raw_value.parse::<u8>().or(Err(ParseError::new(format!(
"[DAT] Invalid status value {} in status channel {} on line {}",
raw_value,
channel_idx + 1,
i + 1
))))?;
status_channels[channel_idx as usize].push_datum(datum);
}
}
self.builder.sample_numbers(sample_numbers);
self.builder.timestamps(timestamps);
Ok(())
}
fn parse_dat_binary16(&mut self) -> ParseResult<()> {
self.builder.sample_numbers(vec![]);
self.builder.timestamps(vec![]);
Ok(())
}
fn parse_dat_binary32(&mut self) -> ParseResult<()> {
self.builder.sample_numbers(vec![]);
self.builder.timestamps(vec![]);
Ok(())
}
fn parse_dat_float32(&mut self) -> ParseResult<()> {
self.builder.sample_numbers(vec![]);
self.builder.timestamps(vec![]);
Ok(())
}
}
fn parse_time_offset(offset_str: &str) -> ParseResult<Option<FixedOffset>> {
let time_value = offset_str.trim();
if time_value.to_lowercase() == "x" {
return Ok(None);
}
let maybe_hours = time_value.parse::<i32>();
if let Ok(hours) = maybe_hours {
return Ok(Some(FixedOffset::east(hours * 3600)));
}
let time_split: Vec<&str> = time_value.split("h").collect();
if time_split.len() != 2 {
return Err(ParseError::new(format!(
"invalid time offset on line: {}",
time_value,
)));
}
let hours = time_split[0]
.trim()
.parse::<i32>()
.or(Err(ParseError::new(format!(
"invalid hour offset in time offset: {} in {}",
time_split[0], time_value,
))))?;
let minutes = time_split[1]
.trim()
.parse::<i32>()
.or(Err(ParseError::new(format!(
"invalid minute offset in time offset: {} in {}",
time_split[1], time_value,
))))?;
let total_offset = if hours > 0 {
hours * 3600 + minutes * 60
} else {
hours * 3600 - minutes * 60
};
return Ok(Some(FixedOffset::east(total_offset)));
}