Scala: наиболее краткое преобразование цветовой строки CSS в целые числа RGB - PullRequest
0 голосов
/ 03 мая 2018

Я пытаюсь получить значения RGB цветовой строки CSS и задаюсь вопросом, насколько хорош мой код:

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

Части этого кода, в которых я больше всего не уверен,

  • match в foldLeft (это хорошая идея использовать интерполяцию строк или код может быть написан короче без интерполяции строк?)
  • 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)
  }

}

Ответы [ 4 ]

0 голосов
/ 08 мая 2018

Если вы ищете краткость, возможно, это решение подойдет (за счет эффективности - подробнее об этом позже):

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 .

0 голосов
/ 04 мая 2018

Вот возможная реализация вашей функции

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]], чтобы сохранить ошибку в случае сбоя.

0 голосов
/ 08 мая 2018

На момент написания этого ответа другие ответы неправильно обрабатывали случаи rgb(), rgba() и именованных цветов. Цветные строки, начинающиеся с хэшей (#), являются лишь частью сделки.

Поскольку у вас есть iText7 в качестве зависимости, а iText7 имеет надстройку pdfHTML, которая означает, что логика синтаксического анализа CSS цветов, очевидно, должна быть где-то в iText7, и, что более важно, она должна обрабатывать разнообразный ассортимент цветных чехлов CSS. Вопрос только в том, чтобы найти правильное место. К счастью, этот API общедоступен и прост в использовании.

Интересующий вас метод - это WebColors.getRGBAColor() из пакета com.itextpdf.kernel.colors, который принимает CSS цветную строку и возвращает массив из 4 элементов со значениями R, G, B, A (последний обозначает альфа, то есть прозрачность).

Вы можете использовать эти значения для немедленного создания цвета (код на 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));
0 голосов
/ 03 мая 2018

Я придумал этот забавный ответ (не проверенный); Я думаю, что самой большой помощью для вас будет использование 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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...