// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// What Python's `datetime` module offers beyond the calendar itself: the ordinal,
// the ISO week date, combining and replacing parts, the epoch both ways, and the
// directive-driven format and parse of `strftime` and `strptime`.
//
// The names are this family's rather than Python's where the two disagree —
// `Date::ordinal` reads better than `toordinal` — but the meanings are the same,
// so a person who knows the Python module knows this one.

///|
/// The earliest and latest dates this calendar writes with a four-digit year.
///
/// Python's `date.min` and `date.max` are 1-01-01 and 9999-12-31; the arithmetic
/// here works outside that range, so these are a convention for writing rather than
/// a limit on computing.
pub let min_date : Date = { year: 1, month: 1, day: 1, }

///|
pub let max_date : Date = { year: 9999, month: 12, day: 31, }

///|
/// Midnight, and the last representable moment of a day.
pub let midnight : Time = { hour: 0, minute: 0, second: 0, nano: 0, }

///|
pub let end_of_day : Time = {
  hour: 23,
  minute: 59,
  second: 59,
  nano: 999_999_999,
}

///|
/// Days since 0001-01-01, that day being 1 (Python's `date.toordinal`).
///
/// The proleptic Gregorian ordinal, which is what makes two dates subtractable
/// without going through an epoch.
pub fn Date::ordinal(self : Date) -> Int {
  self.days() + 719163
}

///|
/// The date with that ordinal (Python's `date.fromordinal`).
pub fn Date::of_ordinal(n : Int) -> Date {
  Date::of_days(n - 719163)
}

///|
/// The ISO 8601 week date: the year the week belongs to, the week number, and the
/// day of the week with Monday as 1 (Python's `date.isocalendar`).
///
/// The year is not always the calendar year. A week belongs to the year that holds
/// its Thursday, so the first of January can fall in the last week of the year
/// before — which is exactly the case a hand-rolled week number gets wrong.
pub fn Date::isocalendar(self : Date) -> (Int, Int, Int) {
  let weekday = self.isoweekday()
  // The Thursday of this week decides which year the week counts against.
  let thursday = self.plus_days(4 - weekday)
  let year = thursday.year
  let jan1 = { year, month: 1, day: 1, }
  let week = (thursday.days() - jan1.days()) / 7 + 1
  (year, week, weekday)
}

///|
/// The day of the week with Monday as 1 and Sunday as 7 (Python's `isoweekday`).
///
/// [`Date::weekday`] counts from Sunday as 0, which is what cron and C's `tm_wday`
/// use; ISO counts from Monday as 1. Both are here because both are asked for, and
/// picking one silently is how an off-by-one gets into a schedule.
pub fn Date::isoweekday(self : Date) -> Int {
  let d = self.weekday()
  if d == 0 {
    7
  } else {
    d
  }
}

///|
/// The date named by an ISO week date (Python's `date.fromisocalendar`).
pub fn Date::of_isocalendar(
  year : Int,
  week : Int,
  weekday : Int,
) -> Date raise Refused {
  if week < 1 || week > 53 {
    raise Range(field="week", got=week, low=1, high=53)
  }
  if weekday < 1 || weekday > 7 {
    raise Range(field="weekday", got=weekday, low=1, high=7)
  }
  let jan4 : Date = { year, month: 1, day: 4, }
  // The fourth of January is always in week one, so the week's Monday follows.
  let week1 = jan4.plus_days(1 - jan4.isoweekday())
  week1.plus_days((week - 1) * 7 + weekday - 1)
}

///|
/// A date and a time as one wall-clock moment (Python's `datetime.combine`).
///
/// `zone` makes it an instant instead; leaving it off keeps it a wall clock, which
/// is what an opening time or an alarm is.
pub fn Moment::combine(date : Date, time : Time, zone? : Zone) -> Moment {
  match zone {
    Some(z) => Zoned(date~, time~, zone=z)
    None => Plain(date~, time~)
  }
}

///|
/// The same date with some parts replaced (Python's `date.replace`).
pub fn Date::replace(
  self : Date,
  year? : Int,
  month? : Int,
  day? : Int,
) -> Date raise Refused {
  Date::new(
    match year {
      Some(v) => v
      None => self.year
    },
    match month {
      Some(v) => v
      None => self.month
    },
    match day {
      Some(v) => v
      None => self.day
    },
  )
}

///|
/// The same time with some parts replaced (Python's `time.replace`).
pub fn Time::replace(
  self : Time,
  hour? : Int,
  minute? : Int,
  second? : Int,
  nano? : Int,
) -> Time raise Refused {
  Time::new(
    match hour {
      Some(v) => v
      None => self.hour
    },
    match minute {
      Some(v) => v
      None => self.minute
    },
    second=match second {
      Some(v) => v
      None => self.second
    },
    nano=match nano {
      Some(v) => v
      None => self.nano
    },
  )
}

///|
/// The moment `seconds` after 1970-01-01T00:00:00Z (Python's
/// `datetime.fromtimestamp`, with `tz` always supplied).
///
/// Reading the clock is not this package's job — it has no I/O — so a caller that
/// wants "now" passes what its own clock said.
pub fn Moment::of_epoch(
  seconds : Int64,
  nano? : Int = 0,
  zone? : Zone = Utc,
) -> Moment {
  let offset = match zone {
    Utc => 0L
    Offset(m) => m.to_int64() * 60L
  }
  let wall = seconds + offset
  let mut days = wall / 86400L
  let mut rest = wall % 86400L
  if rest < 0L {
    rest = rest + 86400L
    days = days - 1L
  }
  Zoned(
    date=Date::of_days(days.to_int()),
    time={
      hour: (rest / 3600L).to_int(),
      minute: (rest / 60L % 60L).to_int(),
      second: (rest % 60L).to_int(),
      nano,
    },
    zone~,
  )
}

///|
/// One date less another, as a span of whole days (Python's `date - date`).
pub fn Date::diff(self : Date, other : Date) -> Span {
  Span::of(day, (self.days() - other.days()).to_int64())
}

///|
/// One moment less another, for the two shapes that can be subtracted.
///
/// `None` when they are not comparable: an instant and a wall clock differ by an
/// amount that depends on a zone nobody supplied, and a bare time and a bare date
/// are not the same kind of thing.
pub fn Moment::diff(self : Moment, other : Moment) -> Span? {
  match (self.epoch(), other.epoch()) {
    (Some(a), Some(b)) => Some(Span::of(second, a - b))
    _ =>
      match (self, other) {
        (Plain(date~, time~), Plain(date=d2, time=t2)) =>
          Some({
            nanos: (date.days() - d2.days()).to_int64() * day.nanos +
            time.nanos() -
            t2.nanos(),
          })
        (Day(date~), Day(date=d2)) => Some(date.diff(d2))
        (Clock(time~), Clock(time=t2)) =>
          Some({ nanos: time.nanos() - t2.nanos(), })
        _ => None
      }
  }
}

///|
/// The span as seconds, fraction included (Python's `timedelta.total_seconds`).
pub fn Span::total_seconds(self : Span) -> Double {
  self.nanos.to_double() / 1.0e9
}

///|
/// The whole days in a span, and the seconds and nanoseconds left over — the three
/// fields Python's `timedelta` normalises to.
pub fn Span::parts(self : Span) -> (Int, Int, Int) {
  let mut days = self.nanos / day.nanos
  let mut rest = self.nanos % day.nanos
  if rest < 0L {
    rest = rest + day.nanos
    days = days - 1L
  }
  (
    days.to_int(),
    (rest / 1_000_000_000L).to_int(),
    (rest % 1_000_000_000L).to_int(),
  )
}

// -- strftime and strptime ----------------------------------------------------

///|
/// A moment written by a format string of `%` directives (Python's `strftime`).
///
/// The directives are the ones C89 defines and Python implements, less the two
/// that need a locale (`%c`, `%x`, `%X` are the C locale's) and the one that needs
/// a zone database (`%Z` writes `UTC` or the offset, never a name like `CEST`,
/// because a name cannot be recovered from an offset).
///
/// | | |
/// |:--:|:--|
/// | `%Y` `%y` `%m` `%d` | year, two-digit year, month, day |
/// | `%H` `%I` `%M` `%S` `%f` | hour, twelve-hour, minute, second, microseconds |
/// | `%p` `%j` `%a` `%A` `%b` `%B` | AM/PM, day of year, day and month names |
/// | `%z` `%Z` | offset as `+HHMM`, and `UTC` |
/// | `%G` `%V` `%u` `%w` | ISO year, ISO week, ISO weekday, weekday from Sunday |
/// | `%%` | a per cent sign |
pub fn Moment::strftime(self : Moment, format : StringView) -> String {
  let (d, t, z) = match self {
    Zoned(date~, time~, zone~) => (date, time, Some(zone))
    Plain(date~, time~) => (date, time, None)
    Day(date~) => (date, midnight, None)
    Clock(time~) => ({ year: 1900, month: 1, day: 1, }, time, None)
  }
  let f = format.to_owned()
  let out = StringBuilder()
  let mut i = 0
  while i < f.length() {
    if f[i].to_int() != 0x25 || i + 1 >= f.length() {
      out.write_char(f[i].unsafe_to_char())
      i = i + 1
      continue
    }
    let c = f[i + 1].unsafe_to_char()
    i = i + 2
    let (iso_year, iso_week, iso_day) = d.isocalendar()
    out.write_string(
      match c {
        'Y' => d.year.to_string()
        'y' => pad(d.year % 100, 2)
        'm' => pad(d.month, 2)
        'd' => pad(d.day, 2)
        'H' => pad(t.hour, 2)
        'I' => pad(if t.hour % 12 == 0 { 12 } else { t.hour % 12 }, 2)
        'M' => pad(t.minute, 2)
        'S' => pad(t.second, 2)
        'f' => pad(t.nano / 1000, 6)
        'p' => if t.hour < 12 { "AM" } else { "PM" }
        'j' => pad(d.yearday(), 3)
        'a' => weekdays[d.weekday()]
        'A' => long_days[d.weekday()]
        'b' => months[d.month - 1]
        'B' => long_months[d.month - 1]
        'G' => iso_year.to_string()
        'V' => pad(iso_week, 2)
        'u' => iso_day.to_string()
        'w' => d.weekday().to_string()
        'z' =>
          match z {
            Some(Utc) => "+0000"
            Some(Offset(m)) => {
              let sign = if m < 0 { "-" } else { "+" }
              let a = if m < 0 { -m } else { m }
              sign + pad(a / 60, 2) + pad(a % 60, 2)
            }
            None => ""
          }
        'Z' =>
          match z {
            Some(_) => "UTC"
            None => ""
          }
        '%' => "%"
        other => "%" + other.to_string()
      },
    )
  }
  out.to_string()
}

///|
/// A moment read by a format string of `%` directives (Python's `strptime`).
///
/// The directives are the ones [`Moment::strftime`] writes. Literal characters in
/// the format must match the text exactly, except that whitespace in the format
/// matches any run of whitespace, which is what C's `strptime` does and what makes
/// a format survive a double space.
///
/// The result is `Zoned` when the text carried an offset, `Plain` when it carried a
/// date and a time, `Day` when only a date, and `Clock` when only a time — the same
/// four shapes [`parse`] answers with.
pub fn strptime(src : StringView, format : StringView) -> Moment raise Refused {
  let s = src.to_owned()
  let f = format.to_owned()
  let mut year = 1900
  let mut month = 1
  let mut mday = 1
  let mut hour = 0
  let mut minute = 0
  let mut sec = 0
  let mut nano = 0
  let mut offset : Int? = None
  let mut pm : Bool? = None
  let mut saw_date = false
  let mut saw_time = false
  let mut i = 0
  let mut j = 0
  while i < f.length() {
    let fc = f[i].to_int()
    if fc == 0x20 || fc == 0x09 {
      while j < s.length() && (s[j].to_int() == 0x20 || s[j].to_int() == 0x09) {
        j = j + 1
      }
      i = i + 1
      continue
    }
    if fc != 0x25 {
      if j >= s.length() || s[j].to_int() != fc {
        raise Malformed("text does not match the format at \{j}")
      }
      i = i + 1
      j = j + 1
      continue
    }
    if i + 1 >= f.length() {
      raise Malformed("a % at the end of the format")
    }
    let c = f[i + 1].unsafe_to_char()
    i = i + 2
    match c {
      'Y' => {
        let (v, k) = take(s, j, 4)
        year = v
        j = k
        saw_date = true
      }
      'y' => {
        let (v, k) = take(s, j, 2)
        // Python's rule: 69 and over are 1900s, under are 2000s.
        year = if v >= 69 { 1900 + v } else { 2000 + v }
        j = k
        saw_date = true
      }
      'm' => {
        let (v, k) = take(s, j, 2)
        month = v
        j = k
        saw_date = true
      }
      'd' => {
        let (v, k) = take(s, j, 2)
        mday = v
        j = k
        saw_date = true
      }
      'H' | 'I' => {
        let (v, k) = take(s, j, 2)
        hour = v
        j = k
        saw_time = true
      }
      'M' => {
        let (v, k) = take(s, j, 2)
        minute = v
        j = k
        saw_time = true
      }
      'S' => {
        let (v, k) = take(s, j, 2)
        sec = v
        j = k
        saw_time = true
      }
      'f' => {
        let start = j
        let mut v = 0
        while j < s.length() && is_digit(s[j].to_int()) && j - start < 9 {
          v = v * 10 + (s[j].to_int() - 48)
          j = j + 1
        }
        if j == start {
          raise Malformed("expected digits for %f at \{start}")
        }
        for k = j - start; k < 9; k = k + 1 {
          v = v * 10
        }
        nano = v
        saw_time = true
      }
      'j' => {
        let (v, k) = take(s, j, 3)
        j = k
        // Day of the year sets the date once the year is known.
        let base : Date = { year, month: 1, day: 1, }
        let d = base.plus_days(v - 1)
        month = d.month
        mday = d.day
        saw_date = true
      }
      'p' => {
        if j + 2 > s.length() {
          raise Truncated("expected AM or PM")
        }
        let word = s[j:j + 2].to_owned().to_upper()
        pm = match word {
          "AM" => Some(false)
          "PM" => Some(true)
          _ => raise Malformed("expected AM or PM, got " + word)
        }
        j = j + 2
      }
      'a' | 'A' => j = skip_name(s, j)
      'b' | 'B' => {
        let (m, k) = read_month(s, j)
        month = m
        j = k
        saw_date = true
      }
      'z' => {
        let (o, k) = read_offset(s, j)
        offset = Some(o)
        j = k
      }
      'Z' => j = skip_name(s, j)
      '%' => {
        if j >= s.length() || s[j].to_int() != 0x25 {
          raise Malformed("expected a per cent sign at \{j}")
        }
        j = j + 1
      }
      other => raise Malformed("unknown directive %" + other.to_string())
    }
  }
  match pm {
    Some(true) => if hour < 12 { hour = hour + 12 }
    Some(false) => if hour == 12 { hour = 0 }
    None => ()
  }
  let time = Time::new(hour, minute, second=sec, nano~)
  if !saw_date {
    return Clock(time~)
  }
  let date = Date::new(year, month, mday)
  if !saw_time {
    return Day(date~)
  }
  match offset {
    Some(0) => Zoned(date~, time~, zone=Utc)
    Some(m) => Zoned(date~, time~, zone=Offset(m))
    None => Plain(date~, time~)
  }
}

///|
/// Up to `n` digits at `j`, and where reading stopped.
///
/// Fewer than `n` is allowed when what follows is not a digit, because `%m` reads
/// `1-2-3` the way a person writes it.
fn take(s : String, j : Int, n : Int) -> (Int, Int) raise Refused {
  let mut v = 0
  let mut k = j
  while k < s.length() && k - j < n && is_digit(s[k].to_int()) {
    v = v * 10 + (s[k].to_int() - 48)
    k = k + 1
  }
  if k == j {
    raise Malformed("expected a digit at \{j}")
  }
  (v, k)
}

///|
fn is_digit(c : Int) -> Bool {
  c >= 0x30 && c <= 0x39
}

///|
/// Past a run of letters, for a name this parser does not need to keep.
fn skip_name(s : String, j : Int) -> Int {
  let mut k = j
  while k < s.length() {
    let c = s[k].to_int()
    let letter = (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a)
    if !letter {
      break
    }
    k = k + 1
  }
  k
}

///|
/// A month name at `j`, long form or short, as its number.
fn read_month(s : String, j : Int) -> (Int, Int) raise Refused {
  let end = skip_name(s, j)
  if end == j {
    raise Malformed("expected a month name at \{j}")
  }
  let word = s[j:end].to_owned()
  for k = 0; k < 12; k = k + 1 {
    if long_months[k] == word {
      return (k + 1, end)
    }
  }
  if word.length() >= 3 {
    let three = word[0:3].to_owned()
    for k = 0; k < 12; k = k + 1 {
      if months[k] == three {
        return (k + 1, j + 3)
      }
    }
  }
  raise Malformed("not a month name: " + word)
}

///|
/// An offset at `j`, as `+HHMM`, `+HH:MM` or `Z`.
fn read_offset(s : String, j : Int) -> (Int, Int) raise Refused {
  if j < s.length() && (s[j].to_int() == 0x5a || s[j].to_int() == 0x7a) {
    return (0, j + 1)
  }
  if j + 5 > s.length() {
    raise Truncated("an offset")
  }
  let sign = s[j].to_int()
  if sign != 0x2b && sign != 0x2d {
    raise Malformed("an offset begins with a sign at \{j}")
  }
  let (h, a) = take(s, j + 1, 2)
  let b = if a < s.length() && s[a].to_int() == 0x3a { a + 1 } else { a }
  let (m, c) = take(s, b, 2)
  let total = h * 60 + m
  (if sign == 0x2d { -total } else { total }, c)
}

///|
let long_days : Array[String] = [
  "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
]

///|
let long_months : Array[String] = [
  "January", "February", "March", "April", "May", "June", "July", "August", "September",
  "October", "November", "December",
]

// -- the rest of Python's surface ---------------------------------------------

///|
/// The years Python's calendar spans (`MINYEAR` and `MAXYEAR`).
pub let min_year : Int = 1

///|
pub let max_year : Int = 9999

///|
/// The smallest difference two moments can have (Python's `resolution`).
///
/// A nanosecond rather than Python's microsecond: the clock here is finer, which is
/// what a protocol timestamp and a benchmark both want.
pub let resolution : Span = nanosecond

///|
/// The date out of a moment (Python's `datetime.date()`).
///
/// `None` for a bare time, which has no date to give.
pub fn Moment::date(self : Moment) -> Date? {
  match self {
    Zoned(date~, ..) | Plain(date~, ..) | Day(date~) => Some(date)
    Clock(_) => None
  }
}

///|
/// The time out of a moment (Python's `datetime.time()`).
///
/// Midnight for a bare date, which is the time it names.
pub fn Moment::time(self : Moment) -> Time {
  match self {
    Zoned(time~, ..) | Plain(time~, ..) | Clock(time~) => time
    Day(_) => midnight
  }
}

///|
/// The zone a moment carries, or `None` when it carries none (Python's `tzinfo`).
pub fn Moment::zone(self : Moment) -> Zone? {
  match self {
    Zoned(zone~, ..) => Some(zone)
    _ => None
  }
}

///|
/// How far the moment's clock is from UTC (Python's `utcoffset`).
pub fn Moment::utcoffset(self : Moment) -> Span? {
  match self.zone() {
    Some(Utc) => Some(Span::of(minute, 0L))
    Some(Offset(m)) => Some(Span::of(minute, m.to_int64()))
    None => None
  }
}

///|
/// The same instant read in another zone (Python's `astimezone`).
///
/// `None` for the three shapes that are not instants: moving a wall clock to
/// another zone would be inventing the zone it was written in.
pub fn Moment::astimezone(self : Moment, zone : Zone) -> Moment? {
  match self.epoch() {
    Some(secs) => Some(Moment::of_epoch(secs, nano=self.time().nano, zone~))
    None => None
  }
}

///|
/// The same moment with some parts replaced (Python's `datetime.replace`).
pub fn Moment::replace(
  self : Moment,
  year? : Int,
  month? : Int,
  day? : Int,
  hour? : Int,
  minute? : Int,
  second? : Int,
  nano? : Int,
  zone? : Zone,
) -> Moment raise Refused {
  let d = match self.date() {
    Some(d) => Some(d.replace(year?, month?, day?))
    None => None
  }
  let t = self.time().replace(hour?, minute?, second?, nano?)
  match (d, zone, self) {
    (Some(date), Some(z), _) => Zoned(date~, time=t, zone=z)
    (Some(date), None, Zoned(zone~, ..)) => Zoned(date~, time=t, zone~)
    (Some(date), None, Day(_)) => Day(date~)
    (Some(date), None, _) => Plain(date~, time=t)
    (None, _, _) => Clock(time=t)
  }
}

///|
/// The day of the week of the moment's date, Sunday being 0.
pub fn Moment::weekday(self : Moment) -> Int? {
  self.date().map(d => d.weekday())
}

///|
/// The ISO week date of the moment's date.
pub fn Moment::isocalendar(self : Moment) -> (Int, Int, Int)? {
  self.date().map(d => d.isocalendar())
}

///|
/// Days since 0001-01-01 of the moment's date (Python's `toordinal`).
pub fn Moment::ordinal(self : Moment) -> Int? {
  self.date().map(d => d.ordinal())
}

///|
/// The moment as ISO 8601 text, which is what [`Show`] writes (Python's
/// `isoformat`).
///
/// `sep` is the character between the date and the time, `T` by default; Python
/// allows any, and a space is what a log line wants.
pub fn Moment::isoformat(self : Moment, sep? : Char = 'T') -> String {
  let text = self.to_string()
  if sep == 'T' {
    return text
  }
  let out = StringBuilder()
  let mut swapped = false
  for i = 0; i < text.length(); i = i + 1 {
    if !swapped && text[i].to_int() == 0x54 {
      out.write_char(sep)
      swapped = true
    } else {
      out.write_char(text[i].unsafe_to_char())
    }
  }
  out.to_string()
}

///|
/// A moment read from ISO 8601 text (Python's `fromisoformat`), which is what
/// [`parse`] does.
pub fn of_isoformat(src : StringView) -> Moment raise Refused {
  parse(src)
}

///|
/// The moment in C's `ctime` form: `Sun Nov  6 08:49:37 1994`.
///
/// The day is space-padded rather than zero-padded, which is the one thing that
/// distinguishes this form from every other.
pub fn Moment::ctime(self : Moment) -> String {
  let d = match self.date() {
    Some(d) => d
    None => { year: 1900, month: 1, day: 1, }
  }
  let t = self.time()
  let day_text = if d.day < 10 {
    " " + d.day.to_string()
  } else {
    d.day.to_string()
  }
  weekdays[d.weekday()] +
  " " +
  months[d.month - 1] +
  " " +
  day_text +
  " " +
  pad(t.hour, 2) +
  ":" +
  pad(t.minute, 2) +
  ":" +
  pad(t.second, 2) +
  " " +
  d.year.to_string()
}

///|
/// The nine fields C's `struct tm` holds, in its order (Python's `timetuple`):
/// year, month, day, hour, minute, second, weekday from Monday as 0, day of the
/// year, and whether daylight saving applies — always `-1` here, because that
/// needs a zone database this package does not carry.
pub fn Moment::timetuple(
  self : Moment,
) -> (Int, Int, Int, Int, Int, Int, Int, Int, Int) {
  let d = match self.date() {
    Some(d) => d
    None => { year: 1900, month: 1, day: 1, }
  }
  let t = self.time()
  (
    d.year,
    d.month,
    d.day,
    t.hour,
    t.minute,
    t.second,
    d.isoweekday() - 1,
    d.yearday(),
    -1,
  )
}

///|
/// Seconds since the epoch as a `Double`, fraction included (Python's
/// `datetime.timestamp`).
pub fn Moment::timestamp(self : Moment) -> Double? {
  self.epoch().map(s => s.to_double() + self.time().nano.to_double() / 1.0e9)
}

///|
/// A span made of the parts Python's `timedelta` constructor takes.
pub fn Span::new(
  days? : Int64 = 0,
  hours? : Int64 = 0,
  minutes? : Int64 = 0,
  seconds? : Int64 = 0,
  millis? : Int64 = 0,
  micros? : Int64 = 0,
  nanos? : Int64 = 0,
) -> Span {
  {
    nanos: days * day.nanos +
    hours * hour.nanos +
    minutes * minute.nanos +
    seconds * second.nanos +
    millis * 1_000_000L +
    micros * 1_000L +
    nanos,
  }
}

///|
/// The span negated.
pub fn Span::neg(self : Span) -> Span {
  { nanos: -self.nanos, }
}

///|
/// The span with its sign dropped.
pub fn Span::abs(self : Span) -> Span {
  { nanos: if self.nanos < 0L { -self.nanos } else { self.nanos }, }
}