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

lucuma.core.model.OrcidId.scala Maven / Gradle / Ivy

There is a newer version: 0.108.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.model

import cats.Eq
import io.circe.*

import java.net.URI
import scala.util.matching.Regex

/**
 * The ORCID iD is an https URI with a 16-digit number that is compatible with the ISO Standard
 * (ISO 27729), also known as the International Standard Name Identifier (ISNI). The last digit
 * is a checksum that is either 0-9, or X.
 * @see https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier
 */
sealed abstract case class OrcidId(uri: URI) {

  /**
   * Although the ORCID iD is technically a URI, the portion following `https://orcid.org/` is what
   * we use as the serial form (and what we display to the user).
   */
  final lazy val value: String = uri.getPath().drop(1)

}
object OrcidId {

  private val ValuePat   = raw"(\d{4})-(\d{4})-(\d{4})-(\d{3})([\dX])"
  private val ValueRegex = ValuePat.r
  private val UriRegex   = s"https://orcid.org/$ValuePat".r
  private def parseWith(s: String, re: Regex): Either[String, OrcidId] =
    s match {
      case re(a, b, c, d, x) =>
        if (checkDigit(a + b + c + d) == x)
          Right(new OrcidId(new URI(s"https://orcid.org/$a-$b-$c-$d$x")) {})
        else
          Left(s"Invalid ORCID iD (bad checksum): $s")
      case _ => Left(s"Invalid ORCID iD: $s")
    }

  def fromUri(uri: URI): Either[String, OrcidId] =
    parseWith(uri.toString, UriRegex)

  def fromValue(s: String): Either[String, OrcidId] =
    parseWith(s, ValueRegex)

  given Eq[OrcidId] =
    Eq.by(_.value)

  given Encoder[OrcidId] =
    Encoder[String].contramap(_.value)

  given Decoder[OrcidId] =
    Decoder[String].emap(s => fromValue(s))

  // adapted from the code at the link above
  private[model] def checkDigit(baseDigits: String): String = {
    require(baseDigits.forall(c => c >= '0' && c <= '9'))
    val total = baseDigits.foldLeft(0) { (acc, c) =>
      val digit = c - '0'
      (acc + digit) * 2
    }
    val remainder = total % 11
    val result    = (12 - remainder) % 11
    if (result == 10) "X" else result.toString
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy