<commit_before>import {hexColor, hexColorValues, colorValues, RGBColor, anyColor} from './types'
import {ColorTypes} from './color-types-enum'

/**
 * Utility library for parsing colors, validation, normalization and some other useful features.
 *
 * // Use in TypeScript or a JavaScript module
 * import {ColorUtilities} from '@radiatingstar/color-utilities'
 *
 * // Use in node Node or Browserify
 * const ColorUtilities = require('@radiatingstar/color-utilities')
 *
 * // Create using a `new` keyword or with a factory.
 * const colorUtils = new ColorUtilities()
 * const colorUtilsFromFactory = ColorUtilities.create()
 */
export class ColorUtilities {
  /**
   * A factory method for obtaining an instance of the service.
   * @returns {ColorUtilities} An instance of the service.
   */
  static create(): ColorUtilities {
    return new ColorUtilities()
  }

  /**
   * A dictionary of color values. USes hex values by default.
   */
  static readonly color: {[index: string]: hexColor} = {
    black: '#000000',
    white: '#FFFFFF'
  }

  /* Utilities */

  /**
   * Calculates the relative luminance of a color. Based on the W3C Recommendation.
   * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
   *
   * @param {anyColor} color for which to calculate the luminance
   * @returns {number} relative luminance
   */
  calculateLuminanceOf(color: anyColor): number {
    const multipliers = [0.2126, 0.7152, 0.0722]
    return this.parseColor(color)
      .map(value => value / 255)
      .map(inSRGB => (inSRGB <= 0.03928) ? inSRGB / 12.92 : ((inSRGB + 0.055) / 1.055) ** 2.4)
      .reduce((acc, value, index) => acc + value * multipliers[index], 0)
  }

  /**
   * Calculates a contrast ratio of two colors. Based on the W3C Recommendation. Method ensures the proper
   * order of color values, so it doesn't matter which color is brighter.
   * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
   *
   * @param {anyColor} color1 Calculate this color's contrast ratio…
   * @param {anyColor} color2 to this color.
   * @returns {number} the contrast ratio of provided colors
   */
  calculateContrastRatio(color1: anyColor, color2: anyColor): number {
    return [this.calculateLuminanceOf(color1), this.calculateLuminanceOf(color2)]
      .sort((a, b) => b - a)
      .map(value => value + 0.05)
      .reduce((a, b) => a / b)
  }

  /**
   * Finds out what color encoding type is provided. If a not valid color was provided, the
   * ColorTypes.invalidType is returned.
   *
   * @param {anyColor} color Resolve the type of this color.
   * @returns {ColorTypes} The color's type.
   */
  resolveColorType(color: anyColor): ColorTypes {
    if (this.isValidHexColor(color)) {
      return ColorTypes.hex
    } else if (this.isValidRgbColor(color)) {
      return ColorTypes.rgb
    } else {
      return ColorTypes.invalidType
    }
  }

  /* Parsers */

  /**
   * A generic parser. Resolves the color type before passing it to specific parsers.
   *
   * @throws TypeError
   * @param {anyColor} color A color to be parsed.
   * @returns {colorValues} RGB triplet of the provided hex color
   */
  parseColor(color: anyColor): colorValues {
    switch (this.resolveColorType(color)) {
    case ColorTypes.hex:
      return this.parseHexColor(color)
    case ColorTypes.rgb:
      return this.parseRgbColor(color)
    default:
      throw new TypeError(`Trying to pare an invalid color (${color}).`)
    }
  }

  /**
   * Parses valid hex colors into their RGB representation in base 10.
   *
   * @throws TypeError
   * @param {hexColor} hexColor to be transformed
   * @returns {colorValues} RGB triplet of the provided hex color
   */
  parseHexColor(hexColor: hexColor): colorValues {
    return this.splitHexColor(this.normalizeHexColor(hexColor)).map(this.changeHexToNumber) as colorValues
  }

  /**
   * Transforms the RGB color string into its color values.
   *
   * @throws TypeError
   * @param {RGBColor} rgbColor to be transformed
   * @returns {colorValues} RGB triplet of the provided hex color
   */
  parseRgbColor(rgbColor: RGBColor): colorValues {
    return this.splitRgbColor(this.normalizeRgbColor(rgbColor)) as colorValues
  }

  /* Validators */

  /**
   * Validates the provided color against any specific color type.
   *
   * @param {anyColor?} potentialColor Color to be validated.
   * @returns {boolean} Is this color valid?
   */
  isValidColor(potentialColor?: anyColor): boolean {
    return this.isValidHexColor(potentialColor) || this.isValidRgbColor(potentialColor)
  }

  /**
   * Validates the provided hex color.
   *
   * @example
   * "#FFA500" => true
   * "#FFF" => true
   * "#GGG" => false
   * "" => false
   *
   * @param {hexColor?} potentialHexColor Might-be-valid-or-not hex color string.
   * @returns {boolean} Is it valid?
   */
  isValidHexColor(potentialHexColor?: hexColor): boolean {
    return Boolean(potentialHexColor && /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(potentialHexColor))
  }

  /**
   * Validates the provided RGB color.
   *
   * @example
   * 'rgb(1,1,1)' => true
   * 'rgb(123, 255, 0)' => true
   * ' rgb  (   12,    12,1   )   ' => true
   * 'RGB(255, 0, 0)' => true
   * 'Xrgb(0, 1, 2)' => false
   * 'rgb(256, 255, 255)' => false
   *
   * @param {RGBColor} potentialRgbColor Might-be-valid-or-not rgb color string.
   * @returns {boolean} Is it a valid rgb color string?
   */
  isValidRgbColor(potentialRgbColor?: RGBColor): boolean {
    if (!potentialRgbColor) {
      return false
    } else {
      return this.doesRgbHasValidFormat(potentialRgbColor) && this.areRgbValuesInRange(potentialRgbColor)
    }
  }

  /**
   * Tests if the provided RGB color is in a valid format, eg. "rgb(1, 2, 3)".
   * @param {RGBColor} potentialRgbColor Might-be-valid-or-not rgb color string.
   * @returns {boolean} Does the string has a valid format?
   */
  private doesRgbHasValidFormat(potentialRgbColor: RGBColor): boolean {
    return /^rgb\((\d{1,3}\,){2}\d{1,3}\)$/.test(potentialRgbColor.replace(/\s/g, '').toLowerCase())
  }

  /**
   * Tests if the values of the provided RGB color are in 0-255 range.
   * @param {RGBColor} potentialRgbColor Might-be-valid-or-not rgb color string.
   * @returns {boolean} Are the values are in the RGB range?
   */
  private areRgbValuesInRange(potentialRgbColor: RGBColor): boolean {
    // Typecasting is safe here, since this.isValidRgbColor ensures the valid format first.
    const values = potentialRgbColor.match(/\d{1,3}/g) as string[]
    return values.every(stringValue => !/^0\d/.test(stringValue)) &&
       values.map(stringValue => Number(stringValue)).every(value => value >= 0 && value <= 255)
  }

  /* Analyzers */

  /**
   * Splits the solid hex color string into its value parts.
   *
   * @example
   * "#FFA500" => ["FF", "A5", "00"]
   *
   * @param {hexColor} hexColor to te split
   * @returns {hexColorValues}
   */
  splitHexColor(hexColor: hexColor): hexColorValues {<commit_msg>refactor(lib): extract some methods that are common between HSL and RGB validation<commit_after>import {hexColor, hexColorValues, colorValues, RGBColor, anyColor} from './types'
import {ColorTypes} from './color-types-enum'
import {HSLColor} from './types';

/**
 * Utility library for parsing colors, validation, normalization and some other useful features.
 *
 * // Use in TypeScript or a JavaScript module
 * import {ColorUtilities} from '@radiatingstar/color-utilities'
 *
 * // Use in node Node or Browserify
 * const ColorUtilities = require('@radiatingstar/color-utilities')
 *
 * // Create using a `new` keyword or with a factory.
 * const colorUtils = new ColorUtilities()
 * const colorUtilsFromFactory = ColorUtilities.create()
 */
export class ColorUtilities {
  /**
   * A factory method for obtaining an instance of the service.
   * @returns {ColorUtilities} An instance of the service.
   */
  static create(): ColorUtilities {
    return new ColorUtilities()
  }

  /**
   * A dictionary of color values. USes hex values by default.
   */
  static readonly color: {[index: string]: hexColor} = {
    black: '#000000',
    white: '#FFFFFF'
  }

  /* Utilities */

  /**
   * Calculates the relative luminance of a color. Based on the W3C Recommendation.
   * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
   *
   * @param {anyColor} color for which to calculate the luminance
   * @returns {number} relative luminance
   */
  calculateLuminanceOf(color: anyColor): number {
    const multipliers = [0.2126, 0.7152, 0.0722]
    return this.parseColor(color)
      .map(value => value / 255)
      .map(inSRGB => (inSRGB <= 0.03928) ? inSRGB / 12.92 : ((inSRGB + 0.055) / 1.055) ** 2.4)
      .reduce((acc, value, index) => acc + value * multipliers[index], 0)
  }

  /**
   * Calculates a contrast ratio of two colors. Based on the W3C Recommendation. Method ensures the proper
   * order of color values, so it doesn't matter which color is brighter.
   * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
   *
   * @param {anyColor} color1 Calculate this color's contrast ratio…
   * @param {anyColor} color2 to this color.
   * @returns {number} the contrast ratio of provided colors
   */
  calculateContrastRatio(color1: anyColor, color2: anyColor): number {
    return [this.calculateLuminanceOf(color1), this.calculateLuminanceOf(color2)]
      .sort((a, b) => b - a)
      .map(value => value + 0.05)
      .reduce((a, b) => a / b)
  }

  /**
   * Finds out what color encoding type is provided. If a not valid color was provided, the
   * ColorTypes.invalidType is returned.
   *
   * @param {anyColor} color Resolve the type of this color.
   * @returns {ColorTypes} The color's type.
   */
  resolveColorType(color: anyColor): ColorTypes {
    if (this.isValidHexColor(color)) {
      return ColorTypes.hex
    } else if (this.isValidRgbColor(color)) {
      return ColorTypes.rgb
    } else {
      return ColorTypes.invalidType
    }
  }

  /* Parsers */

  /**
   * A generic parser. Resolves the color type before passing it to specific parsers.
   *
   * @throws TypeError
   * @param {anyColor} color A color to be parsed.
   * @returns {colorValues} RGB triplet of the provided hex color
   */
  parseColor(color: anyColor): colorValues {
    switch (this.resolveColorType(color)) {
    case ColorTypes.hex:
      return this.parseHexColor(color)
    case ColorTypes.rgb:
      return this.parseRgbColor(color)
    default:
      throw new TypeError(`Trying to pare an invalid color (${color}).`)
    }
  }

  /**
   * Parses valid hex colors into their RGB representation in base 10.
   *
   * @throws TypeError
   * @param {hexColor} hexColor to be transformed
   * @returns {colorValues} RGB triplet of the provided hex color
   */
  parseHexColor(hexColor: hexColor): colorValues {
    return this.splitHexColor(this.normalizeHexColor(hexColor)).map(this.changeHexToNumber) as colorValues
  }

  /**
   * Transforms the RGB color string into its color values.
   *
   * @throws TypeError
   * @param {RGBColor} rgbColor to be transformed
   * @returns {colorValues} RGB triplet of the provided hex color
   */
  parseRgbColor(rgbColor: RGBColor): colorValues {
    return this.splitRgbColor(this.normalizeRgbColor(rgbColor)) as colorValues
  }

  /* Validators */

  /**
   * Validates the provided color against any specific color type.
   *
   * @param {anyColor?} potentialColor Color to be validated.
   * @returns {boolean} Is this color valid?
   */
  isValidColor(potentialColor?: anyColor): boolean {
    return this.isValidHexColor(potentialColor) || this.isValidRgbColor(potentialColor)
  }

  /**
   * Validates the provided hex color.
   *
   * @example
   * "#FFA500" => true
   * "#FFF" => true
   * "#GGG" => false
   * "" => false
   *
   * @param {hexColor?} potentialHexColor Might-be-valid-or-not hex color string.
   * @returns {boolean} Is it valid?
   */
  isValidHexColor(potentialHexColor?: hexColor): boolean {
    return Boolean(potentialHexColor && /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(potentialHexColor))
  }

  /**
   * Validates the provided RGB color.
   *
   * @example
   * 'rgb(1,1,1)' => true
   * 'rgb(123, 255, 0)' => true
   * ' rgb  (   12,    12,1   )   ' => true
   * 'RGB(255, 0, 0)' => true
   * 'Xrgb(0, 1, 2)' => false
   * 'rgb(256, 255, 255)' => false
   *
   * @param {RGBColor} potentialRgbColor Might-be-valid-or-not rgb color string.
   * @returns {boolean} Is it a valid rgb color string?
   */
  isValidRgbColor(potentialRgbColor?: RGBColor): boolean {
    if (!potentialRgbColor) {
      return false
    } else {
      return this.isFormatValid(potentialRgbColor, /^rgb\((\d{1,3}\,){2}\d{1,3}\)$/)
        && this.isRangeValid(potentialRgbColor, value => value >= 0 && value <= 255)
    }
  }

  /**
   * Tests if the provided RGB color is in a valid format, eg. "rgb(1, 2, 3)".
   * @param {RGBColor} potentialRgbColor Might-be-valid-or-not rgb color string.
   * @returns {boolean} Does the string has a valid format?
   */
  private doesRgbHasValidFormat(potentialRgbColor: RGBColor): boolean {
    return /^rgb\((\d{1,3}\,){2}\d{1,3}\)$/.test(potentialRgbColor.replace(/\s/g, '').toLowerCase())
  }

  /**
   * Tests if the values of the provided RGB color are in 0-255 range.
   * @param {RGBColor} potentialRgbColor Might-be-valid-or-not rgb color string.
   * @returns {boolean} Are the values are in the RGB range?
   */
  private areRgbValuesInRange(potentialRgbColor: RGBColor): boolean {
    // Typecasting is safe here, since this.isValidRgbColor ensures the valid format first.
    const values = potentialRgbColor.match(/\d{1,3}/g) as string[]
  private trimAndLowercase(color: anyColor): anyColor {
    return color.replace(/\s/g, '').toLowerCase()
  }

  private isRangeValid(color: RGBColor | HSLColor,
                       rangeValidator: (value: number, index?: number) => boolean): boolean {
    const values = color.match(/\d{1,3}/g) as string[]
    return values.every(stringValue => !/^0\d/.test(stringValue)) &&
      values.map(values => Number(values)).every(rangeValidator)
  }

  private isFormatValid(color: RGBColor | HSLColor, validatingRegEx: RegExp): boolean {
    return validatingRegEx.test(this.trimAndLowercase(color))
  }

  /* Analyzers */

  /**
   * Splits the solid hex color string into its value parts.
   *
   * @example
   * "#FFA500" => ["FF", "A5", "00"]
   *
   * @param {hexColor} hexColor to te split
   * @returns {hexColorValues}
   */
  splitHexColor(hexColor: hexColor): hexColorValues {
{'fraction_non_alphanumeric': 0.12821921571320652, 'fraction_numerical': 0.02163312959274775, 'mean_word_length': 3.224543080939948, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 46, 'https://': 4, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3340905', 'n_tokens_mistral': 4911, 'n_tokens_neox': 4591, 'n_words': 1335}