繁体   English   中英

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上运行该示例

我最不确定的那部分代码是

  • foldLeftmatch foldLeft (使用字符串插值是一个好主意,还是可以在没有字符串插值的情况下将代码写得更短?)
  • Integer.parseInttry结合使用(我可以在Scala中使用更漂亮的替代方法吗?) (由于Xavier Guihot的出色评论而得以解决)

但是我希望代码的大部分内容都是可改进的。 除了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)
  }

}

我想出了这个有趣的答案(未经测试)。 我想对您最大的帮助是使用foldLeft 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
}

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

由于将iText7作为依赖项,而iText7具有pdfHTML附加组件,因此,解析CSS颜色的逻辑显然必须在iText7并且更重要的是,它必须处理各种CSS颜色案例。 问题仅在于找到正确的地方。 幸运的是,此API是公开的且易于使用。

你感兴趣的方法是WebColors.getRGBAColor()从包com.itextpdf.kernel.colors它接受一个CSS颜色字符串返回一个4元件阵列R GBA值(最后一个代表α,即透明度)。

您可以使用这些值立即创建颜色(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));

这是您功能的可能实现

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]]以在失败的情况下保留错误,则实现会更干净。

如果您正在寻找简明扼要的方法,那么也许此解决方案可以完成任务(以效率为代价,稍后再介绍):

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 ,在将每个字符加倍后,它将尝试相同的方法。

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

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

美好的事情是简洁,我认为是可读性(就像我说的那样,代码以相当简单的方式映射到问题上,但是可读性当然是旁观者的定义)。

您可以在Scastie上找到此解决方案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM