All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lucuma.core.math.Epoch.scala Maven / Gradle / Ivy

There is a newer version: 0.105.0
Show newest version
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package lucuma.core.math

import cats.Order
import cats.Show
import cats.syntax.all.*
import eu.timepit.refined.*
import eu.timepit.refined.api.Refined
import eu.timepit.refined.cats.*
import eu.timepit.refined.numeric.Interval as RefinedInterval
import eu.timepit.refined.refineV
import lucuma.core.math.parser.EpochParsers
import lucuma.core.optics.Format
import lucuma.core.optics.syntax.all.*
import lucuma.refined.*
import monocle.Prism

import java.time.*

/**
  * An epoch, the astronomer's equivalent of `Instant`, based on a fractional year in some temporal
  * scheme (Julian or Besselian) that determines year zero and the length of a year. The only
  * meaningful operation for an `Epoch` is to ask the elapsed epoch-years between it and some other
  * point in time. We need this for proper motion corrections because velocities are measured in
  * motion per epoch-year. The epoch year is stored internally as integral milliyears.
  * @param scheme This `Epoch`'s temporal scheme.
  * @see The Wikipedia [[https://en.wikipedia.org/wiki/Epoch_(astronomy) article]]
  */
final class Epoch private (val scheme: Epoch.Scheme, val toMilliyears: Epoch.IntMilliYear) {

  /** This `Epoch`'s year. Note that this value is not very useful without the `Scheme`. */
  def epochYear: Double =
    toMilliyears.value.toDouble / 1000.0

  /** Offset in epoch-years from this `Epoch` to the given `Instant`. */
  def untilInstant(i: Instant): Double =
    untilLocalDateTime(LocalDateTime.ofInstant(i, ZoneOffset.UTC))

  /** Offset in epoch-years from this `Epoch` to the given `LocalDateTime`. */
  def untilLocalDateTime(ldt: LocalDateTime): Double =
    untilJulianDay(Epoch.Scheme.toJulianDay(ldt))

  /** Offset in epoch-years from this `Epoch` to the given fractional Julian day. */
  def untilJulianDay(jd: Double): Double =
    untilEpochYear(scheme.fromJulianDayToEpochYears(jd))

  /** Offset in epoch-years from this `Epoch` to the given epoch year under the same scheme. */
  def untilEpochYear(epochYear: Double): Double =
    epochYear - this.epochYear

  def plusYears(y: Double): Option[Epoch] =
    scheme.fromEpochYears(epochYear + y)

  override def equals(a: Any): Boolean =
    a match {
      case e: Epoch => (scheme === e.scheme) && toMilliyears === e.toMilliyears
      case _        => false
    }

  override def hashCode: Int =
    scheme.hashCode ^ toMilliyears.value

  override def toString: String =
    Epoch.fromString.asFormat.taggedToString("Epoch", this)

}

object Epoch extends EpochOptics {
  type Year         = RefinedInterval.Closed[1900, 3000]
  type MilliYear    = RefinedInterval.Closed[1900000, 3000999]
  type IntYear      = Int Refined Year
  type IntMilliYear = Int Refined MilliYear

  /**
    * Standard epoch.
    * @group Constructors
    */
  val J2000: Epoch = Julian.fromIntegralYears(2000.refined[Year])

  /**
    * Standard epoch prior to J2000. Obsolete but still in use.
    * @group Constructors
    */
  val B1950: Epoch = Besselian.fromIntegralYears(1950.refined[Year])

  /**
    * The scheme defines year zero and length of a year in terms of Julian days. There are two
    * common schemes that we support here.
    */
  sealed abstract class Scheme(
    val prefix:       Char,
    val yearBasis:    Double,
    val julianBasis:  Double,
    val lengthOfYear: Double
  ) {

    def fromIntegralYears(years: Epoch.IntYear): Epoch =
      fromMilliyearsUnsafe(years.value * 1000)

    def fromMilliyears(mys: IntMilliYear): Epoch =
      new Epoch(this, mys)

    def fromIntMilliyears(mys: Int): Option[Epoch] =
      refineV[Epoch.MilliYear](mys).map(new Epoch(this, _)).toOption

    def fromMilliyearsUnsafe(mys: Int): Epoch =
      fromIntMilliyears(mys).get

    def fromLocalDateTime(ldt: LocalDateTime): Option[Epoch] =
      fromJulianDay(Scheme.toJulianDay(ldt))

    def fromJulianDay(jd: Double): Option[Epoch] =
      fromEpochYears(fromJulianDayToEpochYears(jd))

    def fromEpochYears(epochYear: Double): Option[Epoch] =
      fromIntMilliyears((epochYear * 1000.0).toInt)

    def fromLocalDateTimeToEpochYears(ldt: LocalDateTime): Double =
      fromJulianDayToEpochYears(Scheme.toJulianDay(ldt))

    def fromJulianDayToEpochYears(jd: Double): Double =
      yearBasis + (jd - julianBasis) / lengthOfYear
  }

  object Scheme {

    /**
      * Convert a `LocalDateTime` to a fractional Julian day.
      * @see The Wikipedia [[https://en.wikipedia.org/wiki/Julian_day article]]
      */
    def toJulianDay(dt: LocalDateTime): Double =
      JulianDate.ofLocalDateTime(dt).dayNumber.toDouble

    given Order[Scheme] =
      Order.by(s => (s.prefix, s.yearBasis, s.julianBasis, s.lengthOfYear))

    given Show[Scheme] =
      Show.fromToString

  }

  /**
    * Module of constructors for Besselian epochs.
    * @group Constructors
    */
  case object Besselian extends Scheme('B', 1900.0, 2415020.31352, 365.242198781)

  /**
    * Module of constructors for Julian epochs.
    * @group Constructors
    */
  case object Julian extends Scheme('J', 2000.0, 2451545.0, 365.25)

  given Order[Epoch] =
    Order.by(e => (e.scheme, e.toMilliyears.value))

  given Show[Epoch] =
    Show.fromToString

}

trait EpochOptics { this: Epoch.type =>

  val fromString: Prism[String, Epoch] =
    Prism[String, Epoch](s => EpochParsers.epoch.parseAll(s).toOption)(e =>
      f"${e.scheme.prefix}%s${e.toMilliyears.value / 1000}%d.${e.toMilliyears.value % 1000}%03d"
    )

  val fromStringNoScheme: Format[String, Epoch] =
    Format(
      s => EpochParsers.epochLenientNoScheme.parseAll(s).toOption,
      {
        case e if e.toMilliyears.value % 1000 === 0 =>
          f"${e.toMilliyears.value / 1000}%d"
        case e if e.toMilliyears.value % 100 === 0  =>
          f"${e.toMilliyears.value / 1000}%d.${(e.toMilliyears.value % 1000) / 100}%01d"
        case e if e.toMilliyears.value % 10 === 0   =>
          f"${e.toMilliyears.value / 1000}%d.${(e.toMilliyears.value % 1000) / 10}%02d"
        case e =>
          f"${e.toMilliyears.value / 1000}%d.${e.toMilliyears.value % 1000}%03d"
      }
    )

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy