reifydb-rql 0.4.7

ReifyDB Query Language (RQL) parser and AST
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2025 ReifyDB

use crate::token::{
	cursor::Cursor,
	token::{Literal, Token, TokenKind},
};

/// Scan for temporal literal (dates/times starting with @)
pub fn scan_temporal<'b>(cursor: &mut Cursor<'b>) -> Option<Token<'b>> {
	if cursor.peek() != Some('@') {
		return None;
	}

	let start_pos = cursor.pos();
	let start_line = cursor.line();
	let start_column = cursor.column();

	cursor.consume(); // consume '@'

	// Accept any sequence of characters that could be part of a temporal
	// literal. This includes letters, digits, colons, hyphens, dots, +, -,
	// /, T, etc.
	let content = cursor.consume_while(|c| {
		c.is_ascii_alphanumeric() || c == '-' || c == ':' || c == '.' || c == '+' || c == '/' || c == 'T'
	});

	if content.is_empty() {
		// Just @ without any content - backtrack
		// We already consumed @, so go back one position
		// Actually, we can't backtrack easily here, so just return None
		// The @ will be caught as an unexpected character
		return None;
	}

	// Create fragment with just the temporal content (excluding @)
	let temporal_start = start_pos + 1; // Skip the '@'

	Some(Token {
		kind: TokenKind::Literal(Literal::Temporal),
		fragment: cursor.make_fragment(
			temporal_start,
			start_line,
			start_column + 1, // +1 for '@'
		),
	})
}

#[cfg(test)]
pub mod tests {
	use Literal::Temporal;

	use super::*;
	use crate::{bump::Bump, token::tokenize};

	#[test]
	fn test_temporal_date() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@2024-01-15").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "2024-01-15");
	}

	#[test]
	fn test_temporal_datetime() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@2024-01-15T10:30:00").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "2024-01-15T10:30:00");
	}

	#[test]
	fn test_temporal_with_timezone() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@2024-01-15T10:30:00+05:30").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "2024-01-15T10:30:00+05:30");
	}

	#[test]
	fn test_temporal_time_only() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@10:30:00").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "10:30:00");
	}

	#[test]
	fn test_temporal_with_microseconds() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@2024-01-15T10:30:00.123456").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "2024-01-15T10:30:00.123456");
	}

	#[test]
	fn test_temporal_alternative_format() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@2024/01/15").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "2024/01/15");
	}

	#[test]
	fn test_temporal_with_trailing() {
		let bump = Bump::new();
		let tokens = tokenize(&bump, "@2024-01-15 rest").unwrap();
		assert_eq!(tokens[0].kind, TokenKind::Literal(Temporal));
		assert_eq!(tokens[0].fragment.text(), "2024-01-15");
		assert_eq!(tokens[1].kind, TokenKind::Identifier);
		assert_eq!(tokens[1].fragment.text(), "rest");
	}

	#[test]
	fn test_invalid_temporal() {
		let bump = Bump::new();
		// Just @ without content should fail to token
		let result = tokenize(&bump, "@");
		assert!(result.is_err(), "@ alone should fail to tokenize");

		// @ followed by invalid characters should fail
		let result = tokenize(&bump, "@#invalid");
		assert!(result.is_err(), "@# should fail to tokenize as # is not valid");

		// @ followed by space should fail since @ alone is not valid
		let result = tokenize(&bump, "@ 2024");
		assert!(result.is_err(), "@ followed by space should fail to tokenize");
	}
}