|
| 1 | +//! `cursor_start` and `cursor_end` values Python certainly accepts, with their ordering keys. |
| 2 | +
|
| 3 | +use crate::model_validation::constants::{ |
| 4 | + CLOCK_FIELD_LENGTH, CLOCK_PART_COUNTS, CLOCK_PARTS_WITH_SECONDS, INTEGER_CURSOR, |
| 5 | + ISO_DATE_LENGTH, ISO_DATE_SEPARATOR_OFFSETS, LAST_SHIFTED_MONTH, MAX_HOUR, MAX_MINUTE, |
| 6 | + MAX_YEAR, MIN_YEAR, TIMESTAMP_CURSOR, UTC_OFFSET_LENGTH, |
| 7 | +}; |
| 8 | +use crate::model_validation::models::Rejected; |
| 9 | +use crate::types::AuthoredNode; |
| 10 | + |
| 11 | +const MICROS_PER_SECOND: i128 = 1_000_000; |
| 12 | +const SECONDS_PER_DAY: i128 = 86_400; |
| 13 | +const SECONDS_PER_HOUR: i128 = 3_600; |
| 14 | +const SECONDS_PER_MINUTE: i128 = 60; |
| 15 | +const CIVIL_EPOCH_OFFSET_DAYS: i128 = 719_468; |
| 16 | +const FRACTION_DIGITS: [usize; 2] = [3, 6]; |
| 17 | + |
| 18 | +/// Return the `_cursor_contract_key` order key: UTC microseconds, or the integer itself. |
| 19 | +pub(crate) fn cursor_bound_key<N: AuthoredNode>( |
| 20 | + value: &N, |
| 21 | + cursor_type: &str, |
| 22 | +) -> Result<i128, Rejected> { |
| 23 | + if let Some(number) = value.integer().filter(|_| cursor_type == INTEGER_CURSOR) { |
| 24 | + return Ok(i128::from(number)); |
| 25 | + } |
| 26 | + let text = value.text().ok_or(Rejected)?; |
| 27 | + match cursor_type { |
| 28 | + INTEGER_CURSOR => integer_key(&text), |
| 29 | + TIMESTAMP_CURSOR => timestamp_key(&text), |
| 30 | + _ => Err(Rejected), |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +/// Return whether two timestamp keys keep their order through Python's float timestamps. |
| 35 | +pub(crate) fn keys_ordered(start: i128, end: i128, cursor_type: &str) -> bool { |
| 36 | + if cursor_type == TIMESTAMP_CURSOR { |
| 37 | + end - start >= MICROS_PER_SECOND |
| 38 | + } else { |
| 39 | + start < end |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +fn integer_key(text: &str) -> Result<i128, Rejected> { |
| 44 | + let digits = text.strip_prefix(['+', '-']).unwrap_or(text); |
| 45 | + if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) { |
| 46 | + return Err(Rejected); |
| 47 | + } |
| 48 | + text.strip_prefix('+') |
| 49 | + .unwrap_or(text) |
| 50 | + .parse() |
| 51 | + .map_err(|_| Rejected) |
| 52 | +} |
| 53 | + |
| 54 | +fn timestamp_key(text: &str) -> Result<i128, Rejected> { |
| 55 | + if !text.is_ascii() { |
| 56 | + return Err(Rejected); |
| 57 | + } |
| 58 | + let clock_start = ISO_DATE_LENGTH + 1; |
| 59 | + let (date, time) = match text.len() { |
| 60 | + ISO_DATE_LENGTH => (text, None), |
| 61 | + length |
| 62 | + if length > clock_start && matches!(text.as_bytes()[ISO_DATE_LENGTH], b'T' | b' ') => |
| 63 | + { |
| 64 | + (&text[..ISO_DATE_LENGTH], Some(&text[clock_start..])) |
| 65 | + } |
| 66 | + _ => return Err(Rejected), |
| 67 | + }; |
| 68 | + let days = date_days(date)?; |
| 69 | + let (micros, offset_seconds) = match time { |
| 70 | + None => (0, 0), |
| 71 | + Some(clock) => clock_micros(clock)?, |
| 72 | + }; |
| 73 | + let utc_micros = (days * SECONDS_PER_DAY - offset_seconds) * MICROS_PER_SECOND + micros; |
| 74 | + let day_micros = SECONDS_PER_DAY * MICROS_PER_SECOND; |
| 75 | + let first_micros = days_from_civil(MIN_YEAR, 1, 1) * day_micros; |
| 76 | + let end_micros = days_from_civil(MAX_YEAR + 1, 1, 1) * day_micros; |
| 77 | + if utc_micros < first_micros || utc_micros >= end_micros { |
| 78 | + return Err(Rejected); |
| 79 | + } |
| 80 | + Ok(utc_micros) |
| 81 | +} |
| 82 | + |
| 83 | +fn date_days(date: &str) -> Result<i128, Rejected> { |
| 84 | + let bytes = date.as_bytes(); |
| 85 | + let [year_end, month_end] = ISO_DATE_SEPARATOR_OFFSETS; |
| 86 | + if bytes[year_end] != b'-' || bytes[month_end] != b'-' { |
| 87 | + return Err(Rejected); |
| 88 | + } |
| 89 | + let year = number(&date[..year_end])?; |
| 90 | + let month = number(&date[year_end + 1..month_end])?; |
| 91 | + let day = number(&date[month_end + 1..])?; |
| 92 | + if year == 0 || !(1..=12).contains(&month) || day == 0 || day > month_days(year, month) { |
| 93 | + return Err(Rejected); |
| 94 | + } |
| 95 | + Ok(days_from_civil(year, month, day)) |
| 96 | +} |
| 97 | + |
| 98 | +fn clock_micros(clock: &str) -> Result<(i128, i128), Rejected> { |
| 99 | + let (clock, offset_seconds) = split_offset(clock)?; |
| 100 | + let (hms, fraction) = clock |
| 101 | + .split_once('.') |
| 102 | + .map_or((clock, None), |(hms, fraction)| (hms, Some(fraction))); |
| 103 | + let parts: Vec<&str> = hms.split(':').collect(); |
| 104 | + if !CLOCK_PART_COUNTS.contains(&parts.len()) |
| 105 | + || parts.iter().any(|part| part.len() != CLOCK_FIELD_LENGTH) |
| 106 | + || (fraction.is_some() && parts.len() != CLOCK_PARTS_WITH_SECONDS) |
| 107 | + { |
| 108 | + return Err(Rejected); |
| 109 | + } |
| 110 | + let hour = number(parts[0])?; |
| 111 | + let minute = number(parts[1])?; |
| 112 | + let second = parts.get(2).map_or(Ok(0), |part| number(part))?; |
| 113 | + if hour > MAX_HOUR || minute > MAX_MINUTE || second > MAX_MINUTE { |
| 114 | + return Err(Rejected); |
| 115 | + } |
| 116 | + let fraction_micros = match fraction { |
| 117 | + None => 0, |
| 118 | + Some(digits) if FRACTION_DIGITS.contains(&digits.len()) => { |
| 119 | + number(digits)? * 10_i128.pow(6 - u32::try_from(digits.len()).map_err(|_| Rejected)?) |
| 120 | + } |
| 121 | + Some(_) => return Err(Rejected), |
| 122 | + }; |
| 123 | + let seconds = hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second; |
| 124 | + Ok(( |
| 125 | + seconds * MICROS_PER_SECOND + fraction_micros, |
| 126 | + offset_seconds, |
| 127 | + )) |
| 128 | +} |
| 129 | + |
| 130 | +fn split_offset(clock: &str) -> Result<(&str, i128), Rejected> { |
| 131 | + if let Some(clock) = clock.strip_suffix('Z') { |
| 132 | + return Ok((clock, 0)); |
| 133 | + } |
| 134 | + let Some(sign_index) = clock.rfind(['+', '-']) else { |
| 135 | + return Ok((clock, 0)); |
| 136 | + }; |
| 137 | + let offset = &clock[sign_index + 1..]; |
| 138 | + let bytes = offset.as_bytes(); |
| 139 | + if offset.len() != UTC_OFFSET_LENGTH || bytes[CLOCK_FIELD_LENGTH] != b':' { |
| 140 | + return Err(Rejected); |
| 141 | + } |
| 142 | + let hours = number(&offset[..CLOCK_FIELD_LENGTH])?; |
| 143 | + let minutes = number(&offset[CLOCK_FIELD_LENGTH + 1..])?; |
| 144 | + if hours > MAX_HOUR || minutes > MAX_MINUTE { |
| 145 | + return Err(Rejected); |
| 146 | + } |
| 147 | + let magnitude = hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE; |
| 148 | + let sign = if clock.as_bytes()[sign_index] == b'-' { |
| 149 | + -1 |
| 150 | + } else { |
| 151 | + 1 |
| 152 | + }; |
| 153 | + Ok((&clock[..sign_index], sign * magnitude)) |
| 154 | +} |
| 155 | + |
| 156 | +fn number(digits: &str) -> Result<i128, Rejected> { |
| 157 | + if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) { |
| 158 | + return Err(Rejected); |
| 159 | + } |
| 160 | + digits.parse().map_err(|_| Rejected) |
| 161 | +} |
| 162 | + |
| 163 | +fn month_days(year: i128, month: i128) -> i128 { |
| 164 | + match month { |
| 165 | + 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29, |
| 166 | + 2 => 28, |
| 167 | + 4 | 6 | 9 | 11 => 30, |
| 168 | + _ => 31, |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +fn days_from_civil(year: i128, month: i128, day: i128) -> i128 { |
| 173 | + let shifted_year = if month <= LAST_SHIFTED_MONTH { |
| 174 | + year - 1 |
| 175 | + } else { |
| 176 | + year |
| 177 | + }; |
| 178 | + let era = shifted_year.div_euclid(400); |
| 179 | + let year_of_era = shifted_year - era * 400; |
| 180 | + let shifted_month = (month + 9) % 12; |
| 181 | + let day_of_year = (153 * shifted_month + 2) / 5 + day - 1; |
| 182 | + let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; |
| 183 | + era * 146_097 + day_of_era - CIVIL_EPOCH_OFFSET_DAYS |
| 184 | +} |
0 commit comments