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
上的示例
我最不确定的代码部分是
foldLeft
中的match
(使用字符串插值是个好主意还是不使用字符串插值代码可以写得更短?)
Integer.parseInt
与 try
结合使用(我可以在 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)
}
}
我想到了这个有趣的答案(未经测试);我想对你最大的帮助是使用 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。
我正在尝试获取 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
上的示例我最不确定的代码部分是
foldLeft
中的match
(使用字符串插值是个好主意还是不使用字符串插值代码可以写得更短?)(由于 Xavier Guihot 的出色评论而解决)Integer.parseInt
与try
结合使用(我可以在 Scala 中使用更漂亮的替代方案吗?)
但我希望我的大部分代码都可以改进。除了 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。