Scala:将 CSS 颜色字符串最简洁地转换为 RGB 整数

Scala: Most concise conversion of a CSS color string to RGB integers

我正在尝试获取 CSS 颜色字符串的 RGB 值,想知道我的代码有多好:

object Color {
  def stringToInts(colorString: String): Option[(Int, Int, Int)] = {
    val trimmedColorString: String = colorString.trim.replaceAll("#", "")
    val longColorString: Option[String] = trimmedColorString.length match {
      // allow only strings with either 3 or 6 letters
      case 3 => Some(trimmedColorString.flatMap(character => s"$character$character"))
      case 6 => Some(trimmedColorString)
      case _ => None
    }
    val values: Option[Seq[Int]] = longColorString.map(_
      .foldLeft(Seq[String]())((accu, character) => accu.lastOption.map(_.toSeq) match {
        case Some(Seq(_, _)) => accu :+ s"$character" // previous value is complete => start with succeeding
        case Some(Seq(c)) => accu.dropRight(1) :+ s"$c$character" // complete the previous value
        case _ => Seq(s"$character") // start with an incomplete first value
      })
      .flatMap(hexString => scala.util.Try(Integer.parseInt(hexString, 16)).toOption)
      // .flatMap(hexString => try {
      //  Some(Integer.parseInt(hexString, 16))
      // } catch {
      //   case _: Exception => None
      // })
    )
    values.flatMap(values => values.size match {
      case 3 => Some((values.head, values(1), values(2)))
      case _ => None
    })
  }
}

// example:

println(Color.stringToInts("#abc")) // prints Some((170,187,204))

您可以 运行 https://scastie.scala-lang.org

上的示例

我最不确定的代码部分是

但我希望我的大部分代码都可以改进。除了 com.itextpdf 之外,我不想引入新的库来缩短我的代码,但是使用 com.itextpdf 函数是一个选项。 (stringToInts的结果要转成new com.itextpdf.kernel.colors.DeviceRgb(...),所以我还是安装了com.itextpdf。)

定义预期功能的测试:

import org.scalatest.{BeforeAndAfterEach, FunSuite}

class ColorTest extends FunSuite with BeforeAndAfterEach {

  test("shorthand mixed case color") {
    val actual: Option[(Int, Int, Int)] = Color.stringToInts("#Fa#F")
    val expected = (255, 170, 255)
    assert(actual === Some(expected))
  }

  test("mixed case color") {
    val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a06")
    val expected = (29, 154, 6)
    assert(actual === Some(expected))
  }

  test("too short long color") {
    val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a6")
    assert(actual === None)
  }

  test("too long shorthand color") {
    val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a")
    assert(actual === None)
  }

  test("invalid color") {
    val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9g06")
    assert(actual === None)
  }

}

我想到了这个有趣的答案(未经测试);我想对你最大的帮助是使用 sliding(2,2) 而不是 foldLeft.

def stringToInts(colorString: String): Option[(Int, Int, Int)] = {
  val trimmedString: String => String = _.trim.replaceAll("#", "")

  val validString: String => Option[String] = s => s.length match {
    case 3 => Some(s.flatMap(c => s"$c$c"))
    case 6 => Some(s)
    case _ => None
  }

  val hex2rgb: String => List[Option[Int]] = _.sliding(2, 2).toList
    .map(hex => Try(Integer.parseInt(hex, 16)).toOption)

  val listOpt2OptTriple: List[Option[Int]] => Option[(Int, Int, Int)] = {
    case Some(r) :: Some(g) :: Some(b) :: Nil => Some(r, g, b)
    case _ => None
  }

  for {
    valid <- validString(trimmedString(colorString))
    rgb = hex2rgb(valid)
    answer <- listOpt2OptTriple(rgb)
  } yield answer
}

这是您的函数的可能实现

def stringToInts(css: String): Option[(Int, Int, Int)] = {
  def cssColour(s: String): Int = {
    val v = Integer.parseInt(s, 16)

    if (s.length == 1) v*16 + v else v
  }

  val s = css.trim.replaceAll("#", "")
  val l = s.length/3

  if (l > 2 || l*3 != s.length) {
    None
  } else {
    Try{
      val res = s.grouped(l).map(cssColour).toSeq

      (res(0), res(1), res(2))
    }.toOption
  }
}

如果它返回 Option[List[Int]] 甚至 Try[List[Int]] 以在失败的情况下保留错误,实现会更清晰。

在撰写此答案时,其他答案并未正确处理 rgb()rgba() 和命名颜色的情况。以哈希 (#) 开头的颜色字符串只是交易的一部分。

因为你有 iText7 作为依赖项并且 iText7 有一个 pdfHTML 附加组件,这意味着解析 CSS 颜色的逻辑显然必须在 iText7 而且,更重要的是,它必须处理各种范围的 CSS 颜色情况。问题只是关于找到合适的地方。幸运的是,这个 API 是 public 并且易于使用。

您感兴趣的方法是包 com.itextpdf.kernel.colors 中的 WebColors.getRGBAColor(),它接受 CSS 颜色字符串 returns 一个包含 [=23= 的 4 元素数组], G, B, A 值(最后一个代表 alpha,即透明度)。

您可以使用这些值立即创建颜色(Java 中的代码):

float[] rgbaColor = WebColors.getRGBAColor("#ababab");
Color color = new DeviceRgb(rgbaColor[0], rgbaColor[1], rgbaColor[2]);

在 Scala 中,它必须类似于

val rgbaColor = WebColors.getRGBAColor("#ababab");
val color = new DeviceRgb(rgbaColor(0), rgbaColor(1), rgbaColor(2));

如果您追求简洁,也许这个解决方案可以胜任(以牺牲效率为代价——稍后会详细介绍):

import scala.util.Try

def parseLongForm(rgb: String): Try[(Int, Int, Int)] =
  Try {
    rgb.replace("#", "").
      grouped(2).toStream.filter(_.length == 2).
      map(Integer.parseInt(_, 16)) match { case Stream(r, g, b) => (r, g, b) }
  }

def parseShortForm(rgb: String): Try[(Int, Int, Int)] =
  parseLongForm(rgb.flatMap(List.fill(2)(_)))

def parse(rgb: String): Option[(Int, Int, Int)] =
  parseLongForm(rgb).orElse(parseShortForm(rgb)).toOption

就简洁性而言,这里的每个函数实际上都是一行代码(如果这是您现在正在寻找的东西)。

核心是函数parseLongForm,它试图通过以下方式解析长6字符的长格式:

  • 删除 # 字符
  • 将字符成对分组
  • 过滤掉单独的项目(以防我们有奇数个字符)
  • 解析每一对
  • 与预期结果匹配以提取单个项目

parseLongForm代表了Try失败的可能性,这让我们可以在parseInt或者模式匹配失败的时候优雅的失败。

parse 调用 parseLongForm,如果结果失败 (orElse),则调用 parseShortForm,它只是在将每个字符加倍后尝试相同的方法。

它成功通过了您提供的测试(荣誉,这使得解决问题 变得容易)。

这种方法的主要问题是您仍然会尝试解析长格式,即使从一开始就很清楚它行不通。因此,如果这可能成为您用例的性能瓶颈,则不推荐使用此代码。另一个问题是,虽然这或多或少是隐藏的,但我们正在使用异常进行流量控制(这也会损害性能)。

优点是简洁,而且我认为,可读性(正如我所说,代码以相当直接的方式映射到问题——但可读性,当然,根据定义在旁观者)。

您可以找到此解决方案 on Scastie