簡體   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