All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fm.common.rich.RichLocale.scala Maven / Gradle / Ivy

/*
 * Copyright 2015 Frugal Mechanic (http://frugalmechanic.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fm.common.rich

import com.github.benmanes.caffeine.cache.{CacheLoader, Caffeine, LoadingCache}
import fm.common.Implicits._
import fm.common.JavaConverters._
import fm.common.ImmutableArray
import java.text.Collator
import java.util.ResourceBundle.Control
import java.util.{Comparator, Locale}
import scala.util.Try


object RichLocale {
  private def DefaultCacheSize: Int = 256

  // Needed on Scala 2.11. Other versions of Scala can automatically perform this functional interface conversion
  private implicit def toCacheLoader[K, V](f: K => V): CacheLoader[K, V] = new CacheLoader[K, V] {
    override def load(key: K): V = f(key)
  }
  
  private val LocaleCacheSize: Int = try {
    System.getProperty("fm.common.LocaleCacheSize").toInt
  } catch {
    case _: NumberFormatException => DefaultCacheSize
  }

  private val comparatorCache: LoadingCache[Locale, Comparator[String]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        Option(Collator.getInstance(locale)).map{ c => c.setStrength(Collator.PRIMARY); c.setDecomposition(Collator.CANONICAL_DECOMPOSITION); c }.map{ _.asInstanceOf[Comparator[String]] } getOrElse String.CASE_INSENSITIVE_ORDER
      }
  }
  
  private val stringOrderingCache: LoadingCache[Locale, Ordering[String]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        Ordering.comparatorToOrdering(comparatorCache.get(locale))
      }
  }
  
  private val stringOptionOrderingCache: LoadingCache[Locale, Ordering[Option[String]]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        Ordering.Option(stringOrderingCache.get(locale))
      }
  }
  
  private val reversedComparatorCache: LoadingCache[Locale, Comparator[String]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        comparatorCache.get(locale).reversed()
      }
  }
  
  private val reversedStringOrderingCache: LoadingCache[Locale, Ordering[String]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        Ordering.comparatorToOrdering(reversedComparatorCache.get(locale))
      }
  }
  
  private val reversedStringOptionOrderingCache: LoadingCache[Locale, Ordering[Option[String]]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        Ordering.Option(reversedStringOrderingCache.get(locale))
      }
  }

  // Note: It looks like there is already some caching in Control.getCandidateLocales.  However there might still
  //       be benefit in caching the resulting ImmutableArray here to prevent excessive allocation since it looks like
  //       Control.getCandidateLocales makes an ArrayList copy on every lookup which is not ideal.
  private val candidateLocalesCache: LoadingCache[Locale, ImmutableArray[Locale]] = {
    Caffeine.newBuilder()
      .maximumSize(LocaleCacheSize)
      .build{ (locale: Locale) =>
        // We do not directly use the ResourceBundle class but we do make use of
        // the ResourceBundle.Control class to handle the lookup logic via the
        // getCandidateLocales method.
        val control: Control = Control.getNoFallbackControl(Control.FORMAT_DEFAULT)

        ImmutableArray.copy(control.getCandidateLocales("", locale).asScala)
      }
  }
}

final class RichLocale(val self: Locale) extends AnyVal {

  def isChinese: Boolean = matchesLanguage(Locale.CHINESE)
  def isEnglish: Boolean = matchesLanguage(Locale.ENGLISH)
  def isFrench: Boolean = matchesLanguage(Locale.FRENCH)
  def isGerman: Boolean = matchesLanguage(Locale.GERMAN)
  def isItalian: Boolean = matchesLanguage(Locale.ITALIAN)
  def isJapanese: Boolean = matchesLanguage(Locale.JAPANESE)
  def isKorean: Boolean = matchesLanguage(Locale.KOREAN)

  @inline private def matchesLanguage(other: Locale): Boolean = null != other && other.getLanguage === self.getLanguage

  def languageTag: String = self.toLanguageTag()

  // Note: This is package private since there isn't currently a use case for this method but we have tests setup for it
  /** Does this locale have a valid non-blank language? */
  private[common] def hasNonBlankValidLanguage: Boolean = Try{ self.getISO3Language.isNotNullOrBlank }.getOrElse(false)

  // Note: This is package private since there isn't currently a use case for this method but we have tests setup for it
  /** Does this locale have a valid non-blank country set? */
  private[common] def hasNonBlankValidCountry: Boolean = Try { self.getISO3Country.isNotNullOrBlank }.getOrElse(false)

  /** The Locale is considered valid if there is a valid (or blank) language and a valid (or blank) country */
  def isValid: Boolean = {
    Try {
      self.getISO3Language()
      self.getISO3Country()
    }.isSuccess
  }

  def candidateLocales: ImmutableArray[Locale] = RichLocale.candidateLocalesCache.get(self)
  
  def displayName(implicit locale: Locale): String = self.getDisplayName(locale)
  def displayCountry(implicit locale: Locale): String = self.getDisplayCountry(locale)
  def displayLanguage(implicit locale: Locale): String = self.getDisplayLanguage(locale)
  def displayScript(implicit locale: Locale): String = self.getDisplayScript(locale)
  def displayVariant(implicit locale: Locale): String = self.getDisplayVariant(locale)
  
  def stringComparator: Comparator[String] = RichLocale.comparatorCache.get(self)
  def stringOrdering: Ordering[String] = RichLocale.stringOrderingCache.get(self)
  def stringOptionOrdering: Ordering[Option[String]] = RichLocale.stringOptionOrderingCache.get(self)
  
  def reversedStringComparator: Comparator[String] = RichLocale.reversedComparatorCache.get(self)
  def reversedStringOrdering: Ordering[String] = RichLocale.reversedStringOrderingCache.get(self)
  def reversedStringOptionOrdering: Ordering[Option[String]] = RichLocale.reversedStringOptionOrderingCache.get(self)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy