Skip to content

Commit 11d6365

Browse files
authored
fix millisecond fraction being handled with wrong scale (#65)
* add tests for #61 * fix millisecond fraction being handled with wrong scale * also raise error if fraction too long * additional test cases * update comment * fix doctest
1 parent 72f1c79 commit 11d6365

File tree

5 files changed

+73
-19
lines changed

5 files changed

+73
-19
lines changed

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z");
6666
To control the specifics of time parsing you can use provide a `TimeConfig`:
6767

6868
```rust
69-
use speedate::{DateTime, Date, Time, TimeConfig};
69+
use speedate::{DateTime, Date, Time, TimeConfig, MicrosecondsPrecisionOverflowBehavior};
7070
let dt = DateTime::parse_bytes_with_config(
7171
"1689102037.5586429".as_bytes(),
72-
&TimeConfig::builder().unix_timestamp_offset(Some(0)).build(),
72+
&TimeConfig::builder()
73+
.unix_timestamp_offset(Some(0))
74+
.microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate)
75+
.build(),
7376
).unwrap();
7477
assert_eq!(
7578
dt,

src/date.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl FromStr for Date {
5656

5757
// 2e10 if greater than this, the number is in ms, if less than or equal, it's in seconds
5858
// (in seconds this is 11th October 2603, in ms it's 20th August 1970)
59-
const MS_WATERSHED: i64 = 20_000_000_000;
59+
pub(crate) const MS_WATERSHED: i64 = 20_000_000_000;
6060
// 1600-01-01 as a unix timestamp used for from_timestamp below
6161
const UNIX_1600: i64 = -11_676_096_000;
6262
// 9999-12-31T23:59:59 as a unix timestamp, used as max allowed value below
@@ -272,11 +272,11 @@ impl Date {
272272

273273
pub(crate) fn timestamp_watershed(timestamp: i64) -> Result<(i64, u32), ParseError> {
274274
let ts_abs = timestamp.checked_abs().ok_or(ParseError::DateTooSmall)?;
275-
let (mut seconds, mut microseconds) = if ts_abs > MS_WATERSHED {
276-
(timestamp / 1_000, timestamp % 1_000 * 1000)
277-
} else {
278-
(timestamp, 0)
279-
};
275+
if ts_abs <= MS_WATERSHED {
276+
return Ok((timestamp, 0));
277+
}
278+
let mut seconds = timestamp / 1_000;
279+
let mut microseconds = ((timestamp % 1_000) * 1000) as i32;
280280
if microseconds < 0 {
281281
seconds -= 1;
282282
microseconds += 1_000_000;

src/datetime.rs

+45-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::numbers::{float_parse_bytes, IntFloat};
2-
use crate::TimeConfigBuilder;
1+
use crate::date::MS_WATERSHED;
2+
use crate::{int_parse_bytes, MicrosecondsPrecisionOverflowBehavior, TimeConfigBuilder};
33
use crate::{time::TimeConfig, Date, ParseError, Time};
44
use std::cmp::Ordering;
55
use std::fmt;
@@ -339,14 +339,50 @@ impl DateTime {
339339
pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result<Self, ParseError> {
340340
match Self::parse_bytes_rfc3339_with_config(bytes, config) {
341341
Ok(d) => Ok(d),
342-
Err(e) => match float_parse_bytes(bytes) {
343-
IntFloat::Int(int) => Self::from_timestamp_with_config(int, 0, config),
344-
IntFloat::Float(float) => {
345-
let micro = (float.fract() * 1_000_000_f64).round() as u32;
346-
Self::from_timestamp_with_config(float.floor() as i64, micro, config)
342+
Err(e) => {
343+
let mut split = bytes.splitn(2, |&b| b == b'.');
344+
let Some(timestamp) =
345+
int_parse_bytes(split.next().expect("splitn always returns at least one element"))
346+
else {
347+
return Err(e);
348+
};
349+
let float_fraction = split.next();
350+
debug_assert!(split.next().is_none()); // at most two elements
351+
match float_fraction {
352+
// If fraction exists but is empty (i.e. trailing `.`), allow for backwards compatibility;
353+
// TODO might want to reconsider this later?
354+
Some(b"") | None => Self::from_timestamp_with_config(timestamp, 0, config),
355+
Some(fract) => {
356+
// fraction is either:
357+
// - up to 3 digits of millisecond fractions, i.e. microseconds
358+
// - or up to 6 digits of second fractions, i.e. milliseconds
359+
let max_digits = if timestamp > MS_WATERSHED { 3 } else { 6 };
360+
let Some(fract_integers) = int_parse_bytes(fract) else {
361+
return Err(e);
362+
};
363+
if config.microseconds_precision_overflow_behavior
364+
== MicrosecondsPrecisionOverflowBehavior::Error
365+
&& fract.len() > max_digits
366+
{
367+
return Err(if timestamp > MS_WATERSHED {
368+
ParseError::MillisecondFractionTooLong
369+
} else {
370+
ParseError::SecondFractionTooLong
371+
});
372+
}
373+
// TODO: Technically this is rounding, but this is what the existing
374+
// behaviour already did. Probably this is always better than "truncating"
375+
// so we might want to change MicrosecondsPrecisionOverflowBehavior and
376+
// make other uses also round / deprecate truncating.
377+
let multiple = 10f64.powf(max_digits as f64 - fract.len() as f64);
378+
Self::from_timestamp_with_config(
379+
timestamp,
380+
(fract_integers as f64 * multiple).round() as u32,
381+
config,
382+
)
383+
}
347384
}
348-
IntFloat::Err => Err(e),
349-
},
385+
}
350386
}
351387
}
352388

src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ pub enum ParseError {
118118
SecondFractionTooLong,
119119
/// second fraction digits missing after `.`
120120
SecondFractionMissing,
121+
/// millisecond fraction value is more than 3 digits long
122+
MillisecondFractionTooLong,
121123
/// invalid digit in duration
122124
DurationInvalidNumber,
123125
/// `t` character repeated in duration

tests/main.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,13 @@ param_tests! {
864864
dt_unix1: ok => "1654646400", "2022-06-08T00:00:00";
865865
dt_unix2: ok => "1654646404", "2022-06-08T00:00:04";
866866
dt_unix_float: ok => "1654646404.5", "2022-06-08T00:00:04.500000";
867+
dt_unix_float_limit: ok => "1654646404.123456", "2022-06-08T00:00:04.123456";
868+
dt_unix_float_ms: ok => "1654646404000.5", "2022-06-08T00:00:04.000500";
869+
dt_unix_float_ms_limit: ok => "1654646404123.456", "2022-06-08T00:00:04.123456";
870+
dt_unix_float_empty: ok => "1654646404.", "2022-06-08T00:00:04";
871+
dt_unix_float_ms_empty: ok => "1654646404000.", "2022-06-08T00:00:04";
872+
dt_unix_float_too_long: err => "1654646404.1234567", SecondFractionTooLong;
873+
dt_unix_float_ms_too_long: err => "1654646404123.4567", MillisecondFractionTooLong;
867874
dt_short_date: err => "xxx", TooShort;
868875
dt_short_time: err => "2020-01-01T12:0", TooShort;
869876
dt: err => "202x-01-01", InvalidCharYear;
@@ -1390,7 +1397,10 @@ fn test_datetime_parse_bytes_does_not_add_offset_for_rfc3339() {
13901397
fn test_datetime_parse_unix_timestamp_from_bytes_with_utc_offset() {
13911398
let time = DateTime::parse_bytes_with_config(
13921399
"1689102037.5586429".as_bytes(),
1393-
&(TimeConfigBuilder::new().unix_timestamp_offset(Some(0)).build()),
1400+
&(TimeConfigBuilder::new()
1401+
.unix_timestamp_offset(Some(0))
1402+
.microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate)
1403+
.build()),
13941404
)
13951405
.unwrap();
13961406
assert_eq!(time.to_string(), "2023-07-11T19:00:37.558643Z");
@@ -1400,7 +1410,10 @@ fn test_datetime_parse_unix_timestamp_from_bytes_with_utc_offset() {
14001410
fn test_datetime_parse_unix_timestamp_from_bytes_as_naive() {
14011411
let time = DateTime::parse_bytes_with_config(
14021412
"1689102037.5586429".as_bytes(),
1403-
&(TimeConfigBuilder::new().unix_timestamp_offset(None).build()),
1413+
&(TimeConfigBuilder::new()
1414+
.unix_timestamp_offset(None)
1415+
.microseconds_precision_overflow_behavior(MicrosecondsPrecisionOverflowBehavior::Truncate)
1416+
.build()),
14041417
)
14051418
.unwrap();
14061419
assert_eq!(time.to_string(), "2023-07-11T19:00:37.558643");

0 commit comments

Comments
 (0)