Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions crates/samedec/src/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ where
B: AsRef<OsStr>,
A: IntoIterator<Item = B>,
{
let (issue_ts, purge_ts) = match header.issue_datetime(&Utc::now()) {
Ok(issue_ts) => (
time_to_unix_str(issue_ts),
time_to_unix_str(issue_ts + header.valid_duration()),
),
Err(_e) => ("".to_owned(), "".to_owned()),
};

let now = Utc::now();
let issue_ts = header
.issue_datetime(&now)
.map(|tm| time_to_unix_str(tm))
.unwrap_or_default();
let purge_ts = header
.purge_datetime(&now)
.map(|tm| time_to_unix_str(tm))
.unwrap_or_default();
let locations: Vec<&str> = header.location_str_iter().collect();
let evt = header.event();

Expand Down
231 changes: 196 additions & 35 deletions crates/sameplace/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,26 +389,36 @@ impl MessageHeader {
self.location_str().split('-')
}

/// Message validity duration (Duration)
/// Message validity duration (`Duration`)
///
/// Returns the message validity duration. The message is
/// valid until
/// Returns the message validity duration or "purge time."
/// The duration specifies how long, relative to the
/// [issue time](MessageHeader::issue_datetime), that the
/// message is valid.
///
/// ```ignore
/// msg.issue_datetime().unwrap() + msg.valid_duration()
/// ```
/// The Duration is typically:
///
/// * increments of **15 minutes** for Durations of
/// **1 hour** or less
///
/// * increments of **30 minutes** for Durations longer
/// than **1 hour**
///
/// * no longer than
/// [99.5 hours](https://www.weather.gov/nwr/samealertduration)
///
/// After this time elapses, the message is no longer valid
/// and should not be relayed or alerted to anymore.
/// but sameplace does not enforce any of these restrictions.
///
/// This field represents the validity time of the *message*
/// This field represents the validity duration of the *message*
/// and not the expected duration of the severe condition.
/// Severe conditions may persist after the message expires!
/// (And might be the subject of future messages.)
/// **An expired message may still refer to an ongoing hazard** or
/// event. Expiration merely indicates that the *message* is no
/// longer valid. Clients are encouraged to retain a history of
/// alerts and voice message contents.
///
/// The valid duration is relative to the
/// [`issue_datetime()`](#method.issue_datetime) and *not* the
/// current time.
/// [`issue_datetime()`](MessageHeader::issue_datetime) and
/// *not* the current time.
///
/// Requires `chrono`.
#[cfg(feature = "chrono")]
Expand All @@ -421,14 +431,33 @@ impl MessageHeader {
///
/// Returns the message validity duration or "purge time."
/// This is a tuple of (`hours`, `minutes`).
/// The duration specifies how long, relative to the
/// [issue time](MessageHeader::issue_datetime), that the
/// message is valid.
///
/// The duration is typically:
///
/// * increments of **15 minutes** for durations of
/// **1 hour** or less
///
/// * increments of **30 minutes** for durations longer
/// than **1 hour**
///
/// * no longer than
/// [99.5 hours](https://www.weather.gov/nwr/samealertduration)
///
/// This field represents the validity time of the *message*
/// but sameplace does not enforce any of these restrictions.
///
/// This field represents the validity duration of the *message*
/// and not the expected duration of the severe condition.
/// Severe conditions may persist after the message expires!
/// (And might be the subject of future messages.)
/// **An expired message may still refer to an ongoing hazard** or
/// event. Expiration merely indicates that the *message* is no
/// longer valid. Clients are encouraged to retain a history of
/// alerts and voice message contents.
///
/// The valid duration is relative to the
/// [`issue_daytime_fields()`](#method.issue_daytime_fields).
/// [`issue_datetime()`](MessageHeader::issue_datetime) and
/// *not* the current time.
pub fn valid_duration_fields(&self) -> (u8, u8) {
let dur_str = &self.message[self.offset_time + Self::OFFSET_FROMPLUS_VALIDTIME
..self.offset_time + Self::OFFSET_FROMPLUS_VALIDTIME + 4];
Expand Down Expand Up @@ -471,28 +500,73 @@ impl MessageHeader {
)
}

/// Message purge/expiration datetime (UTC)
///
/// Compute the datetime that the SAME message should be
/// *purged* or discarded. The caller must provide the time
/// that the message was `received`.
///
/// The returned timestamp is rounded per NWSI 10-1712:
///
/// * For [valid durations](MessageHeader::valid_duration) ≤01h00m,
/// the timestamp is rounded to the nearest 15 minutes
///
/// * For valid durations greater than an hour, the timestamp is
/// rounded to the nearest 30 minutes
///
/// An error is returned if we are unable to calculate
/// a valid timestamp. This can happen, for example, if we
/// project a message sent on Julian/Ordinal Day 366 into a
/// year that is not a leap year.
///
/// SAME headers do not include the year of issuance. This makes
/// it impossible to calculate the full datetime of issuance—or
/// purge, for that matter—without a rough idea of the message's
/// true UTC time. It is *unnecessary* for the `received` time
/// to be a precision timestamp. As long as the provided value
/// is within ±90 days of true UTC, the output time will be
/// correct.
///
/// This field represents the expiration time of the *message*
/// and not the expected duration of the severe condition.
/// **An expired message may still refer to an ongoing hazard** or
/// event. Expiration merely indicates that the *message* is no
/// longer valid. Clients are encouraged to retain a history of
/// alerts and voice message contents.
///
/// Requires `chrono`.
#[cfg(feature = "chrono")]
pub fn purge_datetime(
&self,
received: &DateTime<Utc>,
) -> Result<DateTime<Utc>, InvalidDateErr> {
calculate_expire_time(&self.issue_datetime(received)?, &self.valid_duration())
}

/// Is the message expired?
///
/// Given the current time, determine if this message has
/// expired. It is assumed that `now` is within twelve
/// hours of the message issuance time. Twelve hours is
/// the maximum [`duration`](#method.valid_duration) of a
/// SAME message.
/// expired. It is assumed that `now` is within ±90 days of
/// the message's [issuance time](MessageHeader::issue_datetime).
/// The [maximum duration](https://www.weather.gov/nwr/samealertduration)
/// of a SAME message is 99.5 hours.
///
/// An expired message may still refer to an *ongoing hazard*
/// or event! Expiration merely indicates that the message
/// should not be relayed or alerted to anymore.
/// **An expired message may still refer to an ongoing hazard** or
/// event. Expiration merely indicates that the *message* is no
/// longer valid. Clients are encouraged to retain a history of
/// alerts and voice message contents.
///
/// Requires `chrono`.
#[cfg(feature = "chrono")]
pub fn is_expired_at(&self, now: &DateTime<Utc>) -> bool {
match self.issue_datetime(now) {
Ok(issue_ts) => issue_ts + self.valid_duration() < *now,
Err(_e) => false,
if let Ok(purge) = self.purge_datetime(&now) {
purge < *now
} else {
false
}
}

/// Mesage issuance day/time (fields)
/// Message issuance day/time (fields)
///
/// Returns the message issue day and time, as the string
/// `JJJHHMM`,
Expand Down Expand Up @@ -787,6 +861,31 @@ fn calculate_issue_time(
.ok_or(InvalidDateErr {})
}

/// Calculate message expiration time
#[cfg(feature = "chrono")]
fn calculate_expire_time(
issued: &DateTime<Utc>,
purge: &Duration,
) -> Result<DateTime<Utc>, InvalidDateErr> {
use chrono::DurationRound;

const FIFTEEN_MINUTES: Duration = Duration::minutes(15);
const THIRTY_MINUTES: Duration = Duration::minutes(30);
const ONE_HOUR: Duration = Duration::hours(1);

issued
.checked_add_signed(*purge)
.and_then(|purge_unrounded| {
if *purge <= ONE_HOUR {
purge_unrounded.duration_round(FIFTEEN_MINUTES)
} else {
purge_unrounded.duration_round(THIRTY_MINUTES)
}
.ok()
})
.ok_or(InvalidDateErr {})
}

// Create the latest-possible Utc date from year, ordinal, and HMS
#[cfg(feature = "chrono")]
#[inline]
Expand Down Expand Up @@ -863,9 +962,65 @@ mod tests {
calculate_issue_time((84, 25, 59), (2021, 84)).expect_err("should not succeed");
}

#[cfg(feature = "chrono")]
#[test]
fn test_calculate_expire_time_short() {
const FIFTEEN_MINUTES: Duration = Duration::minutes(15);

let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 44, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 0, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);

let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 46, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 0, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);

let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 55, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 15, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);

let issued = Utc.with_ymd_and_hms(2021, 3, 24, 3, 00, 0).unwrap();
assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 15, 0).unwrap(),
calculate_expire_time(&issued, &FIFTEEN_MINUTES).unwrap()
);
}

#[cfg(feature = "chrono")]
#[test]
fn test_calculate_expire_time_long() {
let issued = Utc.with_ymd_and_hms(2021, 3, 24, 2, 53, 0).unwrap();

assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 15, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(15)).unwrap()
);

assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 30, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(30)).unwrap()
);

assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 3, 45, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(45)).unwrap()
);

assert_eq!(
Utc.with_ymd_and_hms(2021, 3, 24, 4, 00, 0).unwrap(),
calculate_expire_time(&issued, &Duration::minutes(60)).unwrap()
);
}

#[test]
fn test_message_header() {
const THREE_LOCATIONS: &str = "ZCZC-WXR-RWT-012345-567890-888990+0351-3662322-NOCALL00-@@@";
const THREE_LOCATIONS: &str = "ZCZC-WXR-RWT-012345-567890-888990+0330-3662322-NOCALL00-@@@";

let mut errs = vec![0u8; THREE_LOCATIONS.len()];
errs[0] = 1u8;
Expand All @@ -885,7 +1040,7 @@ mod tests {
assert_eq!(Originator::NationalWeatherService, msg.originator());
assert_eq!(msg.event_str(), "RWT");
assert_eq!(msg.event().phenomenon(), Phenomenon::RequiredWeeklyTest);
assert_eq!(msg.valid_duration_fields(), (3, 51));
assert_eq!(msg.valid_duration_fields(), (3, 30));
assert_eq!(msg.issue_daytime_fields(), (366, 23, 22));
assert_eq!(msg.callsign(), "NOCALL00");
assert_eq!(msg.parity_error_count(), 6);
Expand All @@ -898,19 +1053,25 @@ mod tests {
// time API checks
#[cfg(feature = "chrono")]
{
// mock system time that the message was received
let received = Utc.with_ymd_and_hms(2020, 12, 31, 11, 30, 34).unwrap();

assert_eq!(
Utc.with_ymd_and_hms(2020, 12, 31, 23, 22, 00).unwrap(),
msg.issue_datetime(&Utc.with_ymd_and_hms(2020, 12, 31, 11, 30, 34).unwrap())
.unwrap()
msg.issue_datetime(&received).unwrap()
);
assert_eq!(
msg.valid_duration(),
Duration::hours(3) + Duration::minutes(51)
Duration::hours(3) + Duration::minutes(30)
);
assert_eq!(
Utc.with_ymd_and_hms(2021, 1, 1, 3, 0, 00).unwrap(),
msg.purge_datetime(&received).unwrap()
);
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2020, 12, 31, 23, 59, 0).unwrap()));
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 1, 20, 30).unwrap()));
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 3, 13, 00).unwrap()));
assert!(msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 3, 13, 01).unwrap()));
assert!(!msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 2, 59, 59).unwrap()));
assert!(msg.is_expired_at(&Utc.with_ymd_and_hms(2021, 1, 1, 3, 0, 01).unwrap()));
}

// try again via Message
Expand Down
2 changes: 1 addition & 1 deletion sample/npt.22050.s16le.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ exec 0>/dev/null
[ "$SAMEDEC_IS_NATIONAL" = "Y" ]

lifetime=$(( SAMEDEC_PURGETIME - SAMEDEC_ISSUETIME))
[ "$lifetime" -eq $(( 30*60 )) ]
[ "$lifetime" -eq $(( 25*60 )) ]

echo "+OK"
2 changes: 1 addition & 1 deletion sample/two_and_two.22050.s16le.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ exec 0>/dev/null
[ "$SAMEDEC_IS_NATIONAL" = "" ]

lifetime=$(( SAMEDEC_PURGETIME - SAMEDEC_ISSUETIME))
[ "$lifetime" -eq $(( 1*60*60 + 30*60 )) ]
[ "$lifetime" -eq $(( 1*60*60 + 36*60 )) ]

echo "+OK"
Loading