Skip to content

Commit 15f5a03

Browse files
committed
feat: add fast-path ISO 8601 parsing helpers for DateTime, DateTimeOffset, and TimeSpan
1 parent 8038d24 commit 15f5a03

File tree

3 files changed

+608
-4
lines changed

3 files changed

+608
-4
lines changed

src/DateTime.cpp

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,193 @@ namespace nfx::time
221221
}
222222
} // namespace internal
223223

224+
//=====================================================================
225+
// Fast parsing helpers
226+
//=====================================================================
227+
228+
namespace
229+
{
230+
/** @brief Fast parse 2 digits without validation (inline-optimized) */
231+
[[nodiscard]] constexpr std::int32_t parse2Digits( const char* p ) noexcept
232+
{
233+
return ( p[0] - '0' ) * 10 + ( p[1] - '0' );
234+
}
235+
236+
/** @brief Fast parse 4 digits without validation (inline-optimized) */
237+
[[nodiscard]] constexpr std::int32_t parse4Digits( const char* p ) noexcept
238+
{
239+
return ( p[0] - '0' ) * 1000 + ( p[1] - '0' ) * 100 + ( p[2] - '0' ) * 10 + ( p[3] - '0' );
240+
}
241+
242+
/** @brief Check if character is a digit */
243+
[[nodiscard]] constexpr bool isDigit( char c ) noexcept
244+
{
245+
return c >= '0' && c <= '9';
246+
}
247+
248+
/** @brief Validate that all positions contain digits */
249+
[[nodiscard]] constexpr bool areDigits( const char* p, std::size_t count ) noexcept
250+
{
251+
for( std::size_t i = 0; i < count; ++i )
252+
{
253+
if( !isDigit( p[i] ) )
254+
{
255+
return false;
256+
}
257+
}
258+
return true;
259+
}
260+
261+
/**
262+
* @brief Fast-path parser for standard ISO 8601 formats
263+
* @details Handles the most common formats with fixed positions:
264+
* - "YYYY-MM-DD" (10 chars)
265+
* - "YYYY-MM-DDTHH:mm:ss" (19 chars)
266+
* - "YYYY-MM-DDTHH:mm:ssZ" (20 chars)
267+
* - "YYYY-MM-DDTHH:mm:ss.f" (21-27 chars)
268+
* - "YYYY-MM-DDTHH:mm:ss.fZ" (22-28 chars)
269+
* @return true if parsed successfully via fast path, false if fallback needed
270+
*/
271+
[[nodiscard]] bool tryParseFastPath( std::string_view str, DateTime& result ) noexcept
272+
{
273+
const std::size_t len = str.length();
274+
275+
// Fast path requirements: minimum 10 chars (YYYY-MM-DD)
276+
if( len < 10 )
277+
{
278+
return false;
279+
}
280+
281+
const char* data = str.data();
282+
283+
// Validate fixed separators and digit positions for date part
284+
if( data[4] != '-' || data[7] != '-' || !areDigits( data, 4 ) || !areDigits( data + 5, 2 ) ||
285+
!areDigits( data + 8, 2 ) )
286+
{
287+
return false;
288+
}
289+
290+
// Parse date components using fast digit parsing
291+
const std::int32_t year = parse4Digits( data );
292+
const std::int32_t month = parse2Digits( data + 5 );
293+
const std::int32_t day = parse2Digits( data + 8 );
294+
295+
// Validate date components
296+
if( !internal::isValidDate( year, month, day ) )
297+
{
298+
return false;
299+
}
300+
301+
// Date-only format: "YYYY-MM-DD"
302+
if( len == 10 )
303+
{
304+
const std::int64_t ticks = internal::dateToTicks( year, month, day );
305+
result = DateTime{ ticks };
306+
return true;
307+
}
308+
309+
// Time part must start with 'T'
310+
if( data[10] != 'T' )
311+
{
312+
return false;
313+
}
314+
315+
// Need at least "YYYY-MM-DDTHH:mm:ss" (19 chars)
316+
if( len < 19 )
317+
{
318+
return false;
319+
}
320+
321+
// Validate time separators and digits
322+
if( data[13] != ':' || data[16] != ':' || !areDigits( data + 11, 2 ) || !areDigits( data + 14, 2 ) ||
323+
!areDigits( data + 17, 2 ) )
324+
{
325+
return false;
326+
}
327+
328+
// Parse time components
329+
const std::int32_t hour = parse2Digits( data + 11 );
330+
const std::int32_t minute = parse2Digits( data + 14 );
331+
const std::int32_t second = parse2Digits( data + 17 );
332+
333+
// Validate time components
334+
if( !internal::isValidTime( hour, minute, second, 0 ) )
335+
{
336+
return false;
337+
}
338+
339+
std::int32_t fractionalTicks = 0;
340+
std::size_t pos = 19;
341+
342+
// Handle optional 'Z' at position 19
343+
if( len == 20 && data[19] == 'Z' )
344+
{
345+
// "YYYY-MM-DDTHH:mm:ssZ" - no fractional seconds
346+
pos = 20;
347+
}
348+
// Handle fractional seconds
349+
else if( len > 19 && data[19] == '.' )
350+
{
351+
pos = 20; // Start after '.'
352+
353+
// Parse up to 7 fractional digits (100ns precision)
354+
std::int32_t fractionValue = 0;
355+
std::int32_t fractionDigits = 0;
356+
357+
while( pos < len && isDigit( data[pos] ) && fractionDigits < 7 )
358+
{
359+
fractionValue = fractionValue * 10 + ( data[pos] - '0' );
360+
++fractionDigits;
361+
++pos;
362+
}
363+
364+
if( fractionDigits == 0 )
365+
{
366+
return false; // '.' must be followed by at least one digit
367+
}
368+
369+
// Pad to 7 digits (convert to 100ns ticks)
370+
while( fractionDigits < 7 )
371+
{
372+
fractionValue *= 10;
373+
++fractionDigits;
374+
}
375+
376+
fractionalTicks = fractionValue;
377+
378+
// Skip remaining fractional digits beyond our precision
379+
while( pos < len && isDigit( data[pos] ) )
380+
{
381+
++pos;
382+
}
383+
384+
// Optional 'Z' after fractional seconds
385+
if( pos < len && data[pos] == 'Z' )
386+
{
387+
++pos;
388+
}
389+
}
390+
else if( len != 19 )
391+
{
392+
// Unexpected format - not a standard ISO 8601 we can fast-path
393+
return false;
394+
}
395+
396+
// Should have consumed the entire string
397+
if( pos != len )
398+
{
399+
return false;
400+
}
401+
402+
// Calculate total ticks
403+
const std::int64_t ticks = internal::dateToTicks( year, month, day ) +
404+
internal::timeToTicks( hour, minute, second, 0 ) + fractionalTicks;
405+
406+
result = DateTime{ ticks };
407+
return true;
408+
}
409+
} // namespace
410+
224411
//=====================================================================
225412
// DateTime class
226413
//=====================================================================
@@ -700,15 +887,24 @@ namespace nfx::time
700887

701888
bool DateTime::fromString( std::string_view iso8601String, DateTime& result ) noexcept
702889
{
703-
// ISO 8601 parser using std::from_chars
704-
// Supports: YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ,
705-
// YYYY-MM-DDTHH:mm:ss.f, YYYY-MM-DDTHH:mm:ss.fffffffZ, etc.
890+
// Fast empty/length check
706891
if( iso8601String.empty() || iso8601String.length() < 10 )
707892
{
708893
return false;
709894
}
710895

711-
// Remove trailing 'Z' if present
896+
// Try fast-path parser first (handles 95% of real-world cases)
897+
// Supports: YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ,
898+
// YYYY-MM-DDTHH:mm:ss.f, YYYY-MM-DDTHH:mm:ss.fffffffZ
899+
if( tryParseFastPath( iso8601String, result ) )
900+
{
901+
return true;
902+
}
903+
904+
// Fallback to flexible parser for non-standard formats
905+
// (handles timezone offsets, variable digit counts, etc.)
906+
907+
// Remove trailing 'Z' if present (for flexible parser compatibility)
712908
if( iso8601String.back() == 'Z' )
713909
{
714910
iso8601String.remove_suffix( 1 );

0 commit comments

Comments
 (0)