├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bench ├── bench.sbt ├── project │ ├── build.properties │ ├── plugins.sbt │ └── scalac.sbt ├── scalac.sbt └── src │ └── main │ └── scala │ └── io │ └── jvm │ └── uuid │ └── bench │ ├── FromStringBenchmark.scala │ └── ToStringBenchmark.scala ├── build.sbt ├── notes ├── 0.1.7.markdown ├── 0.2.0.markdown ├── 0.2.1.markdown ├── 0.2.2.markdown ├── 0.2.3.markdown ├── 0.2.4.markdown ├── 0.3.0.markdown ├── 0.3.1.markdown └── about.markdown ├── project ├── build.properties ├── plugins.sbt ├── publish.sbt.disabled └── scalac.sbt ├── publish.sbt ├── scalac.sbt └── src ├── main └── scala │ └── io │ └── jvm │ └── uuid │ ├── Imports.scala │ ├── RichUUID.scala │ ├── StaticUUID.scala │ └── package.scala └── test └── scala └── test └── io └── jvm └── uuid ├── ImportFeatureSpec.scala ├── UUIDFeatureCheck.scala └── UUIDFeatureSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .classpath 3 | .idea 4 | .project 5 | .settings 6 | .target 7 | 8 | target 9 | 10 | private.sbt 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | branches: 4 | only: 5 | - 2.13.x 6 | - 2.12.x 7 | - 2.11.x 8 | - 2.10.x 9 | - 2.9.x 10 | - 2.8.x 11 | 12 | scala: 13 | - "2.13.0" 14 | 15 | jdk: 16 | - openjdk12 17 | - openjdk11 18 | - openjdk10 19 | - openjdk9 20 | - openjdk8 21 | - oraclejdk12 22 | - oraclejdk11 23 | - oraclejdk9 24 | 25 | sudo: false 26 | 27 | notifications: 28 | email: 29 | recipients: 30 | - marko.elezovic@oradian.com 31 | 32 | script: 33 | # Try to build documentation, package sources and test coverage 34 | - sbt ++$TRAVIS_SCALA_VERSION publishLocal clean coverage test coverageReport 35 | 36 | after_success: 37 | - bash <(curl -s https://codecov.io/bash) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Oradian Ltd. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 3. Neither the name of the Oradian Ltd. nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-uuid 2 | [![Build Status](https://travis-ci.org/melezov/scala-uuid.svg?branch=2.13.x)](https://travis-ci.org/melezov/scala-uuid) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.jvm.uuid/scala-uuid_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.jvm.uuid/scala-uuid_2.13) 4 | [![Scaladoc](https://javadoc-badge.appspot.com/io.jvm.uuid/scala-uuid_2.13.svg?label=scaladoc)](http://javadoc-badge.appspot.com/io.jvm.uuid/scala-uuid_2.13) 5 | [![License](https://img.shields.io/badge/license-BSD%203--Clause-brightgreen.svg)](https://opensource.org/licenses/BSD-3-Clause) 6 | [![Codecov](https://img.shields.io/codecov/c/github/melezov/scala-uuid/2.13.x.svg)](http://codecov.io/github/melezov/scala-uuid?branch=2.13.x) 7 | [![Codacy](https://api.codacy.com/project/badge/786c3c5e6fe24eed85733fd1848eef7e)](https://www.codacy.com/app/melezov/scala-uuid) 8 | 9 | An optimized Scala wrapper for `java.util.UUID` - inspired by [scala-time](https://github.com/jorgeortiz85/scala-time/ "A Scala wrapper for Joda Time"). 10 | 11 | Cross-building is caring - latest version (`0.3.1`) has been published against all versions of Scala: 12 | [**2.8.x**](https://github.com/melezov/scala-uuid/tree/2.8.x "Go to 2.8.x branch"): 2.8.1, 2.8.2 13 | [**2.9.x**](https://github.com/melezov/scala-uuid/tree/2.9.x "Go to 2.9.x branch"): 2.9.0, 2.9.0-1, 2.9.1, 2.9.1-1, 2.9.2, 2.9.3 14 | [**2.10.x**](https://github.com/melezov/scala-uuid/tree/2.10.x "Go to 2.10.x branch"): 2.10.7 15 | [**2.11.x**](https://github.com/melezov/scala-uuid/tree/2.11.x "Go to 2.11.x branch"): 2.11.12 16 | [**2.12.x**](https://github.com/melezov/scala-uuid/tree/2.12.x "Go to 2.12.x branch"): 2.12.9 17 | **2.13.x**: 2.13.0 18 | 19 | #### Installation: 20 | 21 | **scala-uuid** is being published to OSSRH / Maven Central and should be available without adding additional repositories. 22 | To add the library dependency to your project, simply add: 23 | 24 | ```scala 25 | libraryDependencies += "io.jvm.uuid" %% "scala-uuid" % "0.3.1" 26 | ``` 27 | 28 | #### In order to use: 29 | 30 | scala> import io.jvm.uuid._ 31 | import io.jvm.uuid._ 32 | 33 | You will now have the `UUID` type and object available: 34 | 35 | scala> classOf[UUID] 36 | res0: Class[io.jvm.uuid.UUID] = class java.util.UUID 37 | 38 | *Alternatively*, you can extend `io.jvm.uuid.Imports` and bring the implicits into scope that way. 39 | This is fairly useful for [importing via (package) objects](src/test/scala/com/example/ImportFeatureSpec.scala#L32 "Open ImportFeatureSpec source"): 40 | 41 | scala> object MyClass extends io.jvm.uuid.Imports 42 | defined object MyClass 43 | 44 | scala> import MyClass._ 45 | import MyClass._ 46 | 47 | scala> classOf[UUID] 48 | res0: Class[MyClass.UUID] = class java.util.UUID 49 | 50 | #### Constructors: 51 | 52 | scala> UUID.random 53 | res1: io.jvm.uuid.UUID = 5f4bdbef-0417-47a6-ac9e-ffc5a4905f7f 54 | 55 | scala> UUID(1, 2) 56 | res2: io.jvm.uuid.UUID = 00000000-0000-0001-0000-000000000002 57 | 58 | scala> UUID("00112233-4455-6677-8899-aAbBcCdDeEfF") 59 | res3: io.jvm.uuid.UUID = 00112233-4455-6677-8899-aabbccddeeff 60 | 61 | #### Array factory methods: 62 | 63 | scala> UUID(Array[Long](1L, 2L)) 64 | res4: io.jvm.uuid.UUID = 00000000-0000-0001-0000-000000000002 65 | 66 | scala> UUID(Array[Int](1, 2, 3, 4)) 67 | res5: io.jvm.uuid.UUID = 00000001-0000-0002-0000-000300000004 68 | 69 | scala> UUID(Array[Short](1, 2, 3, 4, 5, 6, 7, 8)) 70 | res6: io.jvm.uuid.UUID = 00010002-0003-0004-0005-000600070008 71 | 72 | scala> UUID(Array[Byte](1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)) 73 | res7: io.jvm.uuid.UUID = 01020304-0506-0708-090a-0b0c0d0e0f10 74 | 75 | scala> UUID("5ca1ab1e-Feed-Dead-Beef-CafeBabeC0de".toCharArray) 76 | res8: io.jvm.uuid.UUID = 5ca1ab1e-feed-dead-beef-cafebabec0de 77 | 78 | Bear in mind that the `String` constructor requires an **exact**, 36 character String representation: 79 | 80 | scala> UUID("01020304-0506-0708-090a-0b0c0d0e0f10") 81 | res9: io.jvm.uuid.UUID = 01020304-0506-0708-090a-0b0c0d0e0f10 82 | 83 | scala> UUID("01020304-0506-0708-090a-0b0c0d0e0f1") // missing hexadecimal digit 84 | java.lang.IllegalArgumentException: UUID must be in format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, 85 | where x is a hexadecimal digit (got: 01020304-0506-0708-090a-0b0c0d0e0f1) 86 | at io.jvm.uuid.StaticUUID.apply(StaticUUID.scala:218) 87 | ... 43 elided 88 | 89 | There is a legacy factory method available through providing `false` as the second argument, 90 | but it has a lot of interesting *features*: 91 | 92 | scala> UUID("1-2-3-4-00000000000000004000000000000005", false) 93 | res10: io.jvm.uuid.UUID = 00000001-0002-0003-4004-000000000005 94 | 95 | #### Accessors: 96 | 97 | scala> val foo = UUID.random 98 | foo: io.jvm.uuid.UUID = 17fa3a17-a302-4fd7-81f8-54882cdd7d78 99 | 100 | scala> foo.string 101 | res11: String = 17fa3a17-a302-4fd7-81f8-54882cdd7d78 102 | 103 | scala> foo.mostSigBits 104 | res12: Long = 1727757280243503063 105 | 106 | scala> foo.leastSigBits formatted "%016x" 107 | res13: String = 81f854882cdd7d78 108 | 109 | scala> foo.longArray 110 | res14: Array[Long] = Array(1727757280243503063, -9081415704747606664) 111 | 112 | scala> foo.intArray 113 | res15: Array[Int] = Array(402274839, -1560129577, -2114431864, 752713080) 114 | 115 | scala> foo.shortArray 116 | res16: Array[Short] = Array(6138, 14871, -23806, 20439, -32264, 21640, 11485, 32120) 117 | 118 | scala> foo.byteArray 119 | res17: Array[Byte] = Array(23, -6, 58, 23, -93, 2, 79, -41, -127, -8, 84, -120, 44, -35, 125, 120) 120 | 121 | scala> foo.charArray 122 | res18: Array[Char] = Array(1, 7, f, a, 3, a, 1, 7, -, ..., -, 5, 4, 8, 8, 2, c, d, d, 7, d, 7, 8) 123 | 124 | String accessors are much more optimized than vanilla `toString` ([**7x** speedup](src/main/scala/io/jvm/uuid/RichUUID.scala#L139 "Open RichUUID.scala source")), and come in two flavors: 125 | 126 | scala> foo.string // lower-case by default 127 | res19: String = 17fa3a17-a302-4fd7-81f8-54882cdd7d78 128 | 129 | scala> foo.toLowerCase // explicitly lower-case 130 | res20: String = 17fa3a17-a302-4fd7-81f8-54882cdd7d78 131 | 132 | scala> foo.toUpperCase // explicitly upper case 133 | res21: String = 17FA3A17-A302-4FD7-81F8-54882CDD7D78 134 | 135 | #### Extractors: 136 | 137 | An optimized `String` extractor is also available for your needs (e.g. when extracting URL parameters): 138 | 139 | scala> val SubmitEventRoute = "/event/([^/]+)/submit".r 140 | SubmitEventRoute: scala.util.matching.Regex = /event/([^/]+)/submit 141 | 142 | scala> val SubmitEventRoute(UUID(eventId)) = "/event/EF72505A-A9A6-4CD7-A14C-8F27C96FD727/submit" 143 | eventId: io.jvm.uuid.UUID = ef72505a-a9a6-4cd7-a14c-8f27c96fd727 144 | 145 | #### Unsigned comparison by default: 146 | 147 | **WARNING**: JVM sorts UUIDs differently to the rest of the world (languages and databases). 148 | This is due to default signed Long ordering and has been marked as a `Will Not Fix` 149 | due to legacy code: 150 | https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7025832 151 | 152 | **scala-uuid** provides sanity compatible unsigned ordering by default, compare and contrast: 153 | 154 | scala> new java.util.UUID(0xffffffffffffffffL, 0) compareTo new java.util.UUID(1, 0) 155 | res22: Int = -1 156 | 157 | scala> UUID(0xffffffffffffffffL, 0) compare UUID(1, 0) 158 | res23: Int = 1 159 | 160 | For more information, check out the [feature spec](src/test/scala/test/io/jvm/uuid/UUIDFeatureSpec.scala "Open UUIDFeatureSpec source"). 161 | Contributions are more than welcome! 162 | -------------------------------------------------------------------------------- /bench/bench.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(JmhPlugin) 2 | 3 | libraryDependencies += "io.jvm.uuid" %% "scala-uuid" % "0.3.1" 4 | -------------------------------------------------------------------------------- /bench/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.0-RC3 2 | -------------------------------------------------------------------------------- /bench/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7") 2 | -------------------------------------------------------------------------------- /bench/project/scalac.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.12.9" 2 | scalacOptions ++= Seq( 3 | "-deprecation", 4 | "-encoding", "UTF-8", 5 | "-feature", 6 | "-language:_", 7 | "-unchecked", 8 | "-Xlint", 9 | "-Ywarn-unused:-imports", 10 | ) 11 | -------------------------------------------------------------------------------- /bench/scalac.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.13.0" 2 | scalacOptions in Compile ++= Seq( 3 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 4 | "-encoding", "UTF-8", // Specify character encoding used by source files. 5 | "-target:jvm-1.8", // Target platform for object files. All JVM 1.5 - 1.7 targets are deprecated. (jvm-1.5,jvm-1.6,jvm-1.7,[jvm-1.8]) 6 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 7 | "-Yrangepos", // Use range positions for syntax trees. 8 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 9 | "-Ywarn-value-discard", // Warn when non-Unit expression results are unused. 10 | 11 | "-opt:l:method,inline", // Enable optimizations, `-opt:help' to list choices. 12 | "-opt-inline-from:io.**", // Patterns for classfile names from which to allow inlining, `help` for details. 13 | "-opt-warnings:_", // Enable optimizer warnings 14 | "-Ygen-asmp", "target/asmp", // Generate a parallel output directory of .asmp files (ie ASM Textifier output). 15 | ) 16 | 17 | scalacOptions in Test -= "-opt:l:method,inline" // do not inline tests 18 | -------------------------------------------------------------------------------- /bench/src/main/scala/io/jvm/uuid/bench/FromStringBenchmark.scala: -------------------------------------------------------------------------------- 1 | package io.jvm.uuid 2 | package bench 3 | 4 | import java.util.concurrent.TimeUnit 5 | 6 | import org.openjdk.jmh.annotations._ 7 | 8 | @State(Scope.Thread) 9 | @BenchmarkMode(Array(Mode.AverageTime)) 10 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 11 | class FromStringBenchmark { 12 | var randomUUIDString: String = _ 13 | 14 | @Setup(Level.Invocation) 15 | def setup(): Unit = { 16 | randomUUIDString = UUID.randomString 17 | } 18 | 19 | @Benchmark 20 | def legacyFromString: java.util.UUID = 21 | java.util.UUID.fromString(randomUUIDString) 22 | 23 | @Benchmark 24 | def optimizedApply: java.util.UUID = 25 | UUID(randomUUIDString) 26 | } 27 | -------------------------------------------------------------------------------- /bench/src/main/scala/io/jvm/uuid/bench/ToStringBenchmark.scala: -------------------------------------------------------------------------------- 1 | package io.jvm.uuid 2 | package bench 3 | 4 | import java.util.Locale 5 | import java.util.concurrent.TimeUnit 6 | 7 | import org.openjdk.jmh.annotations._ 8 | 9 | @State(Scope.Thread) 10 | @BenchmarkMode(Array(Mode.AverageTime)) 11 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 12 | class ToStringBenchmark { 13 | var randomUUID: UUID = _ 14 | 15 | @Setup(Level.Invocation) 16 | def setup(): Unit = { 17 | randomUUID = UUID.random 18 | } 19 | 20 | @Benchmark 21 | def legacyToLowerString: String = 22 | randomUUID.toString 23 | 24 | @Benchmark 25 | def optimizedToLowerString: String = 26 | randomUUID.string 27 | 28 | @Benchmark 29 | def legacyToUpperString: String = 30 | randomUUID.toString.toUpperCase(Locale.ROOT) 31 | 32 | @Benchmark 33 | def optimizedToUpperString: String = 34 | randomUUID.toUpperCase 35 | } 36 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization := "io.jvm.uuid" 2 | name := "scala-uuid" 3 | version := "0.3.1" 4 | 5 | unmanagedSourceDirectories in Compile := Seq((scalaSource in Compile).value) 6 | unmanagedSourceDirectories in Test := Seq((scalaSource in Test).value) 7 | 8 | // ### DEPENDENCIES ### // 9 | libraryDependencies += "org.specs2" %% "specs2-scalacheck" % "4.7.0" % Test 10 | 11 | wartremoverWarnings in (Compile, compile) := Warts.allBut( 12 | Wart.Equals, 13 | Wart.ImplicitConversion, 14 | Wart.Null, 15 | Wart.Overloading, 16 | Wart.StringPlusAny, 17 | Wart.Throw, 18 | Wart.Var, 19 | Wart.While, 20 | ) 21 | -------------------------------------------------------------------------------- /notes/0.1.7.markdown: -------------------------------------------------------------------------------- 1 | scala-uuid 0.1.7 is out, now published to **Maven Central** 2 | 3 | ### Changes: 4 | 5 | * New "strict" factory which must accept exactly 36 characters 6 | * Optimized replacement for `toString` (3x speedup over vanilla `toString`) 7 | * Added ScalaCheck for property roundtrip testing 8 | -------------------------------------------------------------------------------- /notes/0.2.0.markdown: -------------------------------------------------------------------------------- 1 | scala-uuid 0.2.0 is out with more optimized goodness! 2 | 3 | ### Breaking changes: 4 | * The primary constructor `UUID(_: String)` now requires a strict, 36 character UUID in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format 5 | * Removed long tuple factory / accessor 6 | * Removed "useless" string extractor 7 | 8 | ### Other changes: 9 | * Added optimized primitive constructors and array factories (`long`, `int`, `short`, `byte`, `char`) 10 | * Inlined static forwarders and utility methods 11 | * `StaticUUID` is now a class, extended by the `StaticUUID` object 12 | * Added ScalaDoc for all methods, linked to GitHub sources 13 | * Now, and forever running with 100% test coverage! 14 | -------------------------------------------------------------------------------- /notes/0.2.1.markdown: -------------------------------------------------------------------------------- 1 | This is a bugfix release! 2 | 3 | ### Changes: 4 | * `unnapply(_: String)` was using a parser which only validated first 36 characters, without a length check 5 | * `apply(_: String)` now also inlines the `Array[Char]` parsing bytecode 6 | -------------------------------------------------------------------------------- /notes/0.2.2.markdown: -------------------------------------------------------------------------------- 1 | Published against Scala 2.12.0! 2 | 3 | ### Changes: 4 | - Added UUID.randomString 5 | - Proguarded against JRE6 (2.11.8) & JRE8 (2.12.0) 6 | -------------------------------------------------------------------------------- /notes/0.2.3.markdown: -------------------------------------------------------------------------------- 1 | Published against Scala 2.12.2! 2 | 3 | ### Changes: 4 | - Optimized .string (7x speedup over java.util.UUID.toString) 5 | - Proguarded against JRE6 (2.11.11) & JRE8 (2.12.2) 6 | -------------------------------------------------------------------------------- /notes/0.2.4.markdown: -------------------------------------------------------------------------------- 1 | Use native UUID.toString if running on Oracle Java 9+ 2 | 3 | ### Changes: 4 | - Add bench project with toString/fromString benchmarks 5 | - Bump Scala versions: Scala 2.13.0-M3, 2.12.4, 2.11.12 & 2.10.7 6 | -------------------------------------------------------------------------------- /notes/0.3.0.markdown: -------------------------------------------------------------------------------- 1 | Add unsigned long comparison support 2 | 3 | ### Changes: 4 | - Make UUID extend Ordered[UUID] 5 | - Expose signed and unsigned Ordering[UUID] 6 | - Inline more helpers 7 | - Bump SBT to 1.2.7 8 | - Bump Scala versions to 2.13.0-M5, 2.12.7 9 | -------------------------------------------------------------------------------- /notes/0.3.1.markdown: -------------------------------------------------------------------------------- 1 | Published against Scala 2.13.0 2 | 3 | ### Changes: 4 | - Bump Scala versions to 2.13.0, 2.12.9 5 | - Bump SBT to 1.3.0-RC3 6 | - Move tests to separate package for real world usage usage (e.g. usage of package private methods) 7 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [scala-uuid](https://github.com/melezov/scala-uuid) is an optimized Scala wrapper for `java.util.UUID` 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.0-RC3 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.2") 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0") 3 | // addSbtPlugin("com.lightbend.sbt" % "sbt-proguard" % "0.3.0") 4 | -------------------------------------------------------------------------------- /project/publish.sbt.disabled: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.5") 2 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") 3 | -------------------------------------------------------------------------------- /project/scalac.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.12.9" 2 | scalacOptions ++= Seq( 3 | "-deprecation", 4 | "-encoding", "UTF-8", 5 | "-feature", 6 | "-language:_", 7 | "-unchecked", 8 | "-Xlint", 9 | "-Ywarn-unused:-imports", 10 | ) 11 | -------------------------------------------------------------------------------- /publish.sbt: -------------------------------------------------------------------------------- 1 | // ### SCALADOC SETTINGS ### // 2 | 3 | scalacOptions in (Compile, doc) ++= Seq( 4 | "-no-link-warnings", 5 | "-sourcepath", (scalaSource in Compile).value.toString, 6 | "-doc-source-url", s"""https://github.com/melezov/scala-uuid/blob/${version.value}-${ 7 | CrossVersion.partialVersion(scalaVersion.value).get.productIterator.mkString(".") 8 | }.x/src/main/scala\u20AC{FILE_PATH}.scala""", 9 | ) 10 | 11 | // ### PUBLISH SETTINGS ### // 12 | 13 | publishTo := Some( 14 | if (version.value endsWith "-SNAPSHOT") 15 | Opts.resolver.sonatypeSnapshots 16 | else 17 | Opts.resolver.sonatypeStaging 18 | ) 19 | 20 | licenses += (("BSD-style", url("http://opensource.org/licenses/BSD-3-Clause"))) 21 | startYear := Some(2013) 22 | 23 | scmInfo := Some(ScmInfo( 24 | url("https://github.com/melezov/scala-uuid"), 25 | "scm:git:https://github.com/melezov/scala-uuid.git", 26 | Some("scm:git:git@github.com:melezov/scala-uuid.git"), 27 | )) 28 | 29 | pomExtra := 30 | 31 | 32 | melezov 33 | Marko Elezović 34 | https://github.com/melezov 35 | 36 | 37 | 38 | publishMavenStyle := true 39 | publishArtifact in Test := false 40 | pomIncludeRepository := { _ => false } 41 | 42 | homepage := Some(url("https://github.com/melezov/scala-uuid")) 43 | 44 | /* 45 | // ### PROGUARD SETTINGS ### // 46 | 47 | enablePlugins(SbtProguard) 48 | proguardVersion in Proguard := "5.3" 49 | 50 | proguardOptions in Proguard := { 51 | val programVer = version.value 52 | val scalaVer = scalaVersion.value 53 | val scalaBinVer = scalaBinaryVersion.value 54 | 55 | val baseDir = baseDirectory.value 56 | val binaryDeps = (proguardBinaryDeps in Proguard).value 57 | 58 | // read lib path from binary relations, failover to default Ivy cache location 59 | val scalaJarName = s"scala-library-${scalaVer}.jar" 60 | val scalaJarFile = binaryDeps find(_.name == scalaJarName) getOrElse ( 61 | Path.userHome / s"/.ivy2/cache/org.scala-lang/scala-library/jars/${scalaJarName}" 62 | ) 63 | 64 | // Try javaHome, then runtime JVM 65 | val jreMin = (javaHome.value).getOrElse( 66 | new File(sys.props("java.home")) 67 | ) 68 | 69 | val runtimeCandidates = Seq("/jre/lib/rt.jar", "/lib/rt.jar") map { jreMin / } 70 | var runtimeJar = runtimeCandidates find { _.exists } getOrElse { 71 | sys.error("Could not locate runtime jar in path: " + jreMin) 72 | } 73 | 74 | val jarName = s"scala-uuid_${scalaBinVer}-${programVer}.jar" 75 | val inJar = s"${baseDir}/target/scala-${scalaBinVer}/${jarName}" 76 | val outJar = s"${baseDir}/target/scala-${scalaBinVer}/proguard/${jarName}" 77 | 78 | s""" 79 | -injars '${inJar}' 80 | -target 1.8 81 | -libraryjars '${runtimeJar}' 82 | -libraryjars '${scalaJarFile}' 83 | -outjars '${outJar}' 84 | -dontobfuscate 85 | -optimizationpasses 32 86 | -keep class io.jvm.uuid.** { *; } 87 | """.trim.split("\n") 88 | } 89 | */ 90 | -------------------------------------------------------------------------------- /scalac.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.13.0" 2 | scalacOptions in Compile ++= Seq( 3 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 4 | "-encoding", "UTF-8", // Specify character encoding used by source files. 5 | "-target:jvm-1.8", // Target platform for object files. All JVM 1.5 - 1.7 targets are deprecated. (jvm-1.5,jvm-1.6,jvm-1.7,[jvm-1.8]) 6 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 7 | "-Yrangepos", // Use range positions for syntax trees. 8 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 9 | "-Ywarn-value-discard", // Warn when non-Unit expression results are unused. 10 | 11 | "-opt:l:method,inline", // Enable optimizations, `-opt:help' to list choices. 12 | "-opt-inline-from:io.**", // Patterns for classfile names from which to allow inlining, `help` for details. 13 | "-opt-warnings:_", // Enable optimizer warnings 14 | "-Ygen-asmp", "target/asmp", // Generate a parallel output directory of .asmp files (ie ASM Textifier output). 15 | ) 16 | 17 | scalacOptions in Test -= "-opt:l:method,inline" // do not inline tests 18 | -------------------------------------------------------------------------------- /src/main/scala/io/jvm/uuid/Imports.scala: -------------------------------------------------------------------------------- 1 | package io.jvm.uuid 2 | 3 | import scala.language.implicitConversions 4 | 5 | /** This trait holds all the components required for completing the pimp-my-library pattern: 6 | * - an `UUID` type alias 7 | * - an `UUID` singleton object with static forwarders and new `UUID` factories 8 | * - an implicit def to provide extensions on the legacy `java.util.UUID` class 9 | * 10 | * Use case is to use it by extending it with your package object: 11 | * {{{ 12 | * package com 13 | * package object example extends io.jvm.uuid.Imports 14 | * }}} 15 | * 16 | * Now any class in the `com.example` package will be able to access rich `UUID` functionality: 17 | * {{{ 18 | * package com.example 19 | * class User(val id: UUID = UUID.random) 20 | * }}} */ 21 | trait Imports { 22 | final type UUID = java.util.UUID 23 | final val UUID: StaticUUID = StaticUUID 24 | 25 | implicit final def toRichUUID(uuid: UUID): RichUUID = 26 | new RichUUID(uuid) 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/io/jvm/uuid/RichUUID.scala: -------------------------------------------------------------------------------- 1 | package io.jvm.uuid 2 | 3 | private[uuid] object RichUUID { 4 | /** Upper-case hexadecimal translation lookup. */ 5 | private val UppercaseLookup: Array[Char] = "0123456789ABCDEF".toCharArray 6 | /** Lower-case hexadecimal translation lookup. */ 7 | private val LowercaseLookup: Array[Char] = "0123456789abcdef".toCharArray 8 | 9 | /** Oracle optimized toString in 9, no sense to compete with future versions */ 10 | private val UseNativeToString: Boolean = 11 | try { 12 | sys.props("java.vendor") == "Oracle Corporation" && 13 | sys.props("java.specification.version").toInt >= 9 14 | } catch { 15 | case _: Exception => false 16 | } 17 | 18 | /** Char buffer to be used by the optimized .string method */ 19 | private val charBuffer: ThreadLocal[Array[Char]] = new ThreadLocal[Array[Char]] { 20 | override def initialValue(): Array[Char] = new Array[Char](36) 21 | } 22 | } 23 | 24 | /** Pimp-my-library pattern, wrapping the underlying `java.util.UUID`. 25 | * 26 | * This class extends AnyVal, making all the extension methods have 27 | * little-to-no runtime overhead. 28 | * 29 | * The pimp is complete through an implicit conversion in the 30 | * [[Imports]] trait or the [[io.jvm.uuid.package uuid]] package object. */ 31 | final class RichUUID private[uuid](private val uuid: UUID) extends AnyVal with Ordered[UUID] { 32 | /** Returns the most significant 64 bits of this `UUID`. */ 33 | @inline def mostSigBits: Long = uuid.getMostSignificantBits 34 | 35 | /** Returns the least significant 64 bits of this `UUID`. */ 36 | @inline def leastSigBits: Long = uuid.getLeastSignificantBits 37 | 38 | /** Encodes this `UUID` as a `Long` array with 2 elements. */ 39 | def longArray: Array[Long] = { 40 | val buffer = new Array[Long](2) 41 | toLongArray(buffer, 0) 42 | buffer 43 | } 44 | 45 | /** Writes this `UUID` to the provided `Long` array. */ 46 | @inline def toLongArray(buffer: Array[Long], offset: Int): Unit = { 47 | buffer(offset ) = uuid.getMostSignificantBits 48 | buffer(offset + 1) = uuid.getLeastSignificantBits 49 | } 50 | 51 | /** Encodes this `UUID` as an `Int` array with 4 elements. */ 52 | def intArray: Array[Int] = { 53 | val buffer = new Array[Int](4) 54 | toIntArray(buffer, 0) 55 | buffer 56 | } 57 | 58 | /** Writes this `UUID` to the provided `Int` array. */ 59 | @inline def toIntArray(buffer: Array[Int], offset: Int): Unit = { 60 | val msb = uuid.getMostSignificantBits 61 | buffer(offset ) = (msb >> 32).toInt 62 | buffer(offset + 1) = msb.toInt 63 | 64 | val lsb = uuid.getLeastSignificantBits 65 | buffer(offset + 2) = (lsb >> 32).toInt 66 | buffer(offset + 3) = lsb.toInt 67 | } 68 | 69 | /** Encodes this `UUID` as a `Short` array with 8 elements. */ 70 | def shortArray: Array[Short] = { 71 | val buffer = new Array[Short](8) 72 | toShortArray(buffer, 0) 73 | buffer 74 | } 75 | 76 | /** Writes this `UUID` to the provided `Short` array. */ 77 | @inline def toShortArray(buffer: Array[Short], offset: Int): Unit = { 78 | val msb = uuid.getMostSignificantBits 79 | val msbh = (msb >> 32).toInt 80 | buffer(offset ) = (msbh >> 16).toShort 81 | buffer(offset + 1) = msbh.toShort 82 | 83 | val msbl = msb.toInt 84 | buffer(offset + 2) = (msbl >> 16).toShort 85 | buffer(offset + 3) = msbl.toShort 86 | 87 | val lsb = uuid.getLeastSignificantBits 88 | val lsbh = (lsb >> 32).toInt 89 | buffer(offset + 4) = (lsbh >> 16).toShort 90 | buffer(offset + 5) = lsbh.toShort 91 | 92 | val lsbl = lsb.toInt 93 | buffer(offset + 6) = (lsbl >> 16).toShort 94 | buffer(offset + 7) = lsbl.toShort 95 | } 96 | 97 | /** Encodes this `UUID` as a `Byte` array with 16 elements. */ 98 | def byteArray: Array[Byte] = { 99 | val buffer = new Array[Byte](16) 100 | toByteArray(buffer, 0) 101 | buffer 102 | } 103 | 104 | /** Writes this `UUID` to the provided `Byte` array. */ 105 | @inline def toByteArray(buffer: Array[Byte], offset: Int): Unit = { 106 | val msb = uuid.getMostSignificantBits 107 | buffer(offset ) = (msb >>> 56).toByte 108 | buffer(offset + 1) = (msb >>> 48).toByte 109 | buffer(offset + 2) = (msb >>> 40).toByte 110 | buffer(offset + 3) = (msb >>> 32).toByte 111 | buffer(offset + 4) = (msb >>> 24).toByte 112 | buffer(offset + 5) = (msb >>> 16).toByte 113 | buffer(offset + 6) = (msb >>> 8).toByte 114 | buffer(offset + 7) = (msb ).toByte 115 | 116 | val lsb = uuid.getLeastSignificantBits 117 | buffer(offset + 8) = (lsb >>> 56).toByte 118 | buffer(offset + 9) = (lsb >>> 48).toByte 119 | buffer(offset + 10) = (lsb >>> 40).toByte 120 | buffer(offset + 11) = (lsb >>> 32).toByte 121 | buffer(offset + 12) = (lsb >>> 24).toByte 122 | buffer(offset + 13) = (lsb >>> 16).toByte 123 | buffer(offset + 14) = (lsb >>> 8).toByte 124 | buffer(offset + 15) = (lsb ).toByte 125 | } 126 | 127 | /** Encodes this `UUID` as a `Char` array with 36 elements in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. */ 128 | def charArray: Array[Char] = { 129 | val buffer = new Array[Char](36) 130 | toCharArrayViaLookup(buffer, 0, RichUUID.LowercaseLookup) 131 | buffer 132 | } 133 | 134 | /** Writes this `UUID` to the provided `Char` array in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. */ 135 | def toCharArray(buffer: Array[Char], offset: Int): Unit = 136 | toCharArrayViaLookup(buffer, offset, RichUUID.LowercaseLookup) 137 | 138 | /** Serializes this `UUID` to the provided `Char` array via a translation matrix. */ 139 | @inline private[this] def toCharArrayViaLookup(buffer: Array[Char], offset: Int, lookup: Array[Char]): Unit = { 140 | val msb = uuid.getMostSignificantBits 141 | val msbh = (msb >>> 32).toInt 142 | buffer(offset ) = lookup((msbh >>> 28) ) 143 | buffer(offset + 1) = lookup((msbh >>> 24) & 0xf) 144 | buffer(offset + 2) = lookup((msbh >>> 20) & 0xf) 145 | buffer(offset + 3) = lookup((msbh >>> 16) & 0xf) 146 | buffer(offset + 4) = lookup((msbh >>> 12) & 0xf) 147 | buffer(offset + 5) = lookup((msbh >>> 8) & 0xf) 148 | buffer(offset + 6) = lookup((msbh >>> 4) & 0xf) 149 | buffer(offset + 7) = lookup((msbh ) & 0xf) 150 | buffer(offset + 8) = '-' 151 | 152 | val msbl = msb.toInt 153 | buffer(offset + 9) = lookup((msbl >>> 28) ) 154 | buffer(offset + 10) = lookup((msbl >>> 24) & 0xf) 155 | buffer(offset + 11) = lookup((msbl >>> 20) & 0xf) 156 | buffer(offset + 12) = lookup((msbl >>> 16) & 0xf) 157 | buffer(offset + 13) = '-' 158 | buffer(offset + 14) = lookup((msbl >>> 12) & 0xf) 159 | buffer(offset + 15) = lookup((msbl >>> 8) & 0xf) 160 | buffer(offset + 16) = lookup((msbl >>> 4) & 0xf) 161 | buffer(offset + 17) = lookup((msbl ) & 0xf) 162 | buffer(offset + 18) = '-' 163 | 164 | val lsb = uuid.getLeastSignificantBits 165 | val lsbh = (lsb >>> 32).toInt 166 | buffer(offset + 19) = lookup((lsbh >>> 28) ) 167 | buffer(offset + 20) = lookup((lsbh >>> 24) & 0xf) 168 | buffer(offset + 21) = lookup((lsbh >>> 20) & 0xf) 169 | buffer(offset + 22) = lookup((lsbh >>> 16) & 0xf) 170 | buffer(offset + 23) = '-' 171 | buffer(offset + 24) = lookup((lsbh >>> 12) & 0xf) 172 | buffer(offset + 25) = lookup((lsbh >>> 8) & 0xf) 173 | buffer(offset + 26) = lookup((lsbh >>> 4) & 0xf) 174 | buffer(offset + 27) = lookup((lsbh ) & 0xf) 175 | 176 | val lsbl = lsb.toInt 177 | buffer(offset + 28) = lookup((lsbl >>> 28) ) 178 | buffer(offset + 29) = lookup((lsbl >>> 24) & 0xf) 179 | buffer(offset + 30) = lookup((lsbl >>> 20) & 0xf) 180 | buffer(offset + 31) = lookup((lsbl >>> 16) & 0xf) 181 | buffer(offset + 32) = lookup((lsbl >>> 12) & 0xf) 182 | buffer(offset + 33) = lookup((lsbl >>> 8) & 0xf) 183 | buffer(offset + 34) = lookup((lsbl >>> 4) & 0xf) 184 | buffer(offset + 35) = lookup((lsbl ) & 0xf) 185 | } 186 | 187 | /** Returns this `UUID` as a `String` in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. 188 | * Hexadecimal characters will be lower-cased. 189 | * This method is an optimized drop in replacement for the legacy `toString` method. */ 190 | def string: String = 191 | if (RichUUID.UseNativeToString) { 192 | uuid.toString 193 | } else { 194 | toStringViaLookup(RichUUID.LowercaseLookup) 195 | } 196 | 197 | /** Alias for `string` which implicitly returns a lower-cased `String`. */ 198 | @inline def toLowerCase: String = string 199 | 200 | /** Returns this `UUID` as a `String` in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. 201 | * Hexadecimal characters will be upper-cased. */ 202 | def toUpperCase: String = toStringViaLookup(RichUUID.UppercaseLookup) 203 | 204 | /** Translate this `UUID` to a `String` via the provided lookup. 205 | * This method should be inlined, to constant-fold the offset. */ 206 | @inline private[this] def toStringViaLookup(lookup: Array[Char]): String = { 207 | val buffer = RichUUID.charBuffer.get() 208 | toCharArrayViaLookup(buffer, 0, lookup) 209 | new String(buffer) // return ownership of the buffer to ThreadLocal 210 | } 211 | 212 | /** WARNING: JVM sorts UUIDs differently to the rest of the world (languages and databases). 213 | * This is due to default signed Long ordering and has been marked as a Will Not Fix 214 | * due to legacy code: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7025832 */ 215 | override def compareTo(that: UUID): Int = uuid compareTo that 216 | 217 | /** This comparison allows for sanity compatible unsigned ordering */ 218 | override def compare(that: UUID): Int = { 219 | val umsb = uuid.getMostSignificantBits 220 | val tmsb = that.getMostSignificantBits 221 | if (umsb != tmsb) { 222 | if (umsb + Long.MinValue < tmsb + Long.MinValue) -1 else 1 223 | } else { 224 | val ulsb = uuid.getLeastSignificantBits 225 | val tlsb = that.getLeastSignificantBits 226 | if (ulsb != tlsb) { 227 | if (ulsb + Long.MinValue < tlsb + Long.MinValue) -1 else 1 228 | } else { 229 | 0 230 | } 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/main/scala/io/jvm/uuid/StaticUUID.scala: -------------------------------------------------------------------------------- 1 | package io.jvm.uuid 2 | 3 | /** Singleton object to be bound with the `UUID` value in the package object. */ 4 | object StaticUUID extends StaticUUID 5 | 6 | /** This class holds all static forwarders and `UUID` factories. 7 | * 8 | * Extend this class from the client code in order to add new functionality to the library. */ 9 | class StaticUUID { 10 | /** Generates a random `UUID` (type 4) using a cryptographically strong pseudo random number generator. */ 11 | @inline final def random: UUID = 12 | java.util.UUID.randomUUID() 13 | 14 | /** Creates a new `UUID` by concatenating two 64-bit values. 15 | * @param mostSigBits Most significant bits of the new `UUID` 16 | * @param leastSigBits Least significant bits of the new `UUID` */ 17 | @inline final def apply(mostSigBits: Long, leastSigBits: Long): UUID = 18 | new UUID(mostSigBits, leastSigBits) 19 | 20 | /** Helper length checker which will be inlined into `UUID` factories. */ 21 | @inline private[this] def lengthCheck(tpe: String, expected: Int, actual: Int): Unit = 22 | if (expected != actual) throwInvalidLength(tpe, expected, actual) 23 | 24 | /** Throws an exception because there was a length mismatch. 25 | * If the actual length was greater than what was expected, suggest using the alternative constructor. */ 26 | private[this] final def throwInvalidLength(tpe: String, expected: Int, actual: Int): Nothing = 27 | throw new IllegalArgumentException( 28 | (if (tpe == "int") "Expecting an " else "Expecting a ") + tpe + " array of length " + expected + ", but length was " + actual + ( 29 | if (actual < expected) " (too short)" else "; if you wish to skip this check use UUID.from" + tpe + "Array instead!" 30 | ) 31 | ) 32 | 33 | /** Creates a new `UUID` by concatenating two 64-bit values. 34 | * @throws IllegalArgumentException In case `buffer.length` != 2 */ 35 | final def apply(buffer: Array[Long]): UUID = { 36 | lengthCheck("Long", 2, buffer.length) 37 | fromLongArray(buffer, 0) 38 | } 39 | 40 | /** Creates a new `UUID` by concatenating two 64-bit values. 41 | * @param offset Offset of the most significant `Long` inside the array. */ 42 | @inline final def fromLongArray(buffer: Array[Long], offset: Int): UUID = 43 | UUID( 44 | buffer(offset ) 45 | , buffer(offset + 1) 46 | ) 47 | 48 | /** Creates a new `UUID` by concatenating four 32-bit values. 49 | * @throws IllegalArgumentException In case `buffer.length != 4` */ 50 | final def apply(buffer: Array[Int]): UUID = { 51 | lengthCheck("Int", 4, buffer.length) 52 | fromIntArray(buffer, 0) 53 | } 54 | 55 | /** Creates a new `UUID` by concatenating four 32-bit values. 56 | * @param offset Offset of the most significant `Int` inside the array. */ 57 | @inline final def fromIntArray(buffer: Array[Int], offset: Int): UUID = { 58 | UUID( 59 | (buffer(offset ).toLong) << 32 60 | | (buffer(offset + 1) & 0xffffffffL) 61 | , (buffer(offset + 2).toLong) << 32 62 | | (buffer(offset + 3) & 0xffffffffL) 63 | ) 64 | } 65 | 66 | /** Creates a new `UUID` by concatenating eight 16-bit values. 67 | * @throws IllegalArgumentException In case `buffer.length != 8` */ 68 | final def apply(buffer: Array[Short]): UUID = { 69 | lengthCheck("Short", 8, buffer.length) 70 | fromShortArray(buffer, 0) 71 | } 72 | 73 | /** Creates a new `UUID` by concatenating eight 16-bit values. 74 | * @param offset Offset of the most significant `Short` inside the array. */ 75 | @inline final def fromShortArray(buffer: Array[Short], offset: Int): UUID = 76 | UUID( 77 | (buffer(offset ) ).toLong << 48 78 | | (buffer(offset + 1) & 0xffffL) << 32 79 | | (buffer(offset + 2) & 0xffffL) << 16 80 | | (buffer(offset + 3) & 0xffffL) 81 | , (buffer(offset + 4) ).toLong << 48 82 | | (buffer(offset + 5) & 0xffffL) << 32 83 | | (buffer(offset + 6) & 0xffffL) << 16 84 | | (buffer(offset + 7) & 0xffffL) 85 | ) 86 | 87 | /** Creates a new `UUID` by concatenating 16 bytes. 88 | * @throws IllegalArgumentException In case `buffer.length != 16` */ 89 | final def apply(buffer: Array[Byte]): UUID = { 90 | lengthCheck("Byte", 16, buffer.length) 91 | fromByteArray(buffer, 0) 92 | } 93 | 94 | /** Creates a new `UUID` by concatenating 16 bytes. 95 | * @param offset Offset of the most significant `Byte` inside the array. */ 96 | @inline final def fromByteArray(buffer: Array[Byte], offset: Int): UUID = 97 | UUID( 98 | (buffer(offset ) ).toLong << 56 99 | | (buffer(offset + 1) & 0xff).toLong << 48 100 | | (buffer(offset + 2) & 0xff).toLong << 40 101 | | (buffer(offset + 3) & 0xff).toLong << 32 102 | | (buffer(offset + 4) & 0xff).toLong << 24 103 | | (buffer(offset + 5) & 0xff) << 16 104 | | (buffer(offset + 6) & 0xff) << 8 105 | | (buffer(offset + 7) & 0xff) 106 | , (buffer(offset + 8) ).toLong << 56 107 | | (buffer(offset + 9) & 0xff).toLong << 48 108 | | (buffer(offset + 10) & 0xff).toLong << 40 109 | | (buffer(offset + 11) & 0xff).toLong << 32 110 | | (buffer(offset + 12) & 0xff).toLong << 24 111 | | (buffer(offset + 13) & 0xff) << 16 112 | | (buffer(offset + 14) & 0xff) << 8 113 | | (buffer(offset + 15) & 0xff) 114 | ) 115 | 116 | /** Hexadecimal character to integer value mapping used in parser. 117 | * Invalid characters are marked with a value of `-1`. */ 118 | private[this] final val Lookup: Array[Int] = { 119 | val buffer = new Array[Int]('f' + 1) 120 | var i = buffer.length 121 | while (i > 0) { 122 | buffer{ i -= 1; i } = i - ( 123 | if (i >= '0' && i <= '9') '0' 124 | else if (i >= 'A' && i <= 'F') 'A' - 10 125 | else if (i >= 'a' /* && i <= 'f' */) 'a' - 10 126 | else i + 1 127 | ) 128 | } 129 | buffer 130 | } 131 | 132 | /** Creates a new `UUID` by parsing 36 chars. 133 | * @throws IllegalArgumentException In case `buffer.length != 36` */ 134 | final def apply(buffer: Array[Char]): UUID = { 135 | lengthCheck("Char", 36, buffer.length) 136 | fromCharArray(buffer, 0) 137 | } 138 | 139 | /** Creates a new `UUID` by parsing 36 chars in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. 140 | * @param offset Position of the first `Char` in the array. */ 141 | @inline final def fromCharArray(buffer: Array[Char], offset: Int): UUID = { 142 | val res = try { 143 | fromCharArrayViaLookup(buffer, offset, Lookup) 144 | } catch { 145 | case _: ArrayIndexOutOfBoundsException => null 146 | } 147 | if (res eq null) throw new IllegalArgumentException( 148 | "UUID must be in format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, where x is a hexadecimal digit (got: " + 149 | new String(buffer, offset, math.min(buffer.length - offset, 36))+ ")") 150 | res 151 | } 152 | 153 | /** Parses an `UUID` in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format from an array of characters, helper utility. 154 | * @throws ArrayIndexOutOfBoundsException in case of character whose value is > max of hex lookup 155 | * Scalac is incompetent when inlining error handling, so this needs to be handled by the callee */ 156 | @inline private[this] final def fromCharArrayViaLookup(buffer: Array[Char], offset: Int, lookup: Array[Int]): UUID = { 157 | val msb3 = ( 158 | (lookup(buffer(offset + 0).toInt) << 12) 159 | | (lookup(buffer(offset + 1).toInt) << 8) 160 | | (lookup(buffer(offset + 2).toInt) << 4) 161 | | (lookup(buffer(offset + 3).toInt) ) 162 | ) 163 | 164 | val msb2 = ( 165 | (lookup(buffer(offset + 4).toInt) << 12) 166 | | (lookup(buffer(offset + 5).toInt) << 8) 167 | | (lookup(buffer(offset + 6).toInt) << 4) 168 | | (lookup(buffer(offset + 7).toInt) ) 169 | ) 170 | 171 | val msb1 = ( 172 | (lookup(buffer(offset + 9).toInt) << 12) 173 | | (lookup(buffer(offset + 10).toInt) << 8) 174 | | (lookup(buffer(offset + 11).toInt) << 4) 175 | | (lookup(buffer(offset + 12).toInt) ) 176 | ) 177 | 178 | val msb0 = ( 179 | (lookup(buffer(offset + 14).toInt) << 12) 180 | | (lookup(buffer(offset + 15).toInt) << 8) 181 | | (lookup(buffer(offset + 16).toInt) << 4) 182 | | (lookup(buffer(offset + 17).toInt) ) 183 | ) 184 | 185 | val lsb3 = ( 186 | (lookup(buffer(offset + 19).toInt) << 12) 187 | | (lookup(buffer(offset + 20).toInt) << 8) 188 | | (lookup(buffer(offset + 21).toInt) << 4) 189 | | (lookup(buffer(offset + 22).toInt) ) 190 | ) 191 | 192 | val lsb2 = ( 193 | (lookup(buffer(offset + 24).toInt) << 12) 194 | | (lookup(buffer(offset + 25).toInt) << 8) 195 | | (lookup(buffer(offset + 26).toInt) << 4) 196 | | (lookup(buffer(offset + 27).toInt) ) 197 | ) 198 | 199 | val lsb1 = ( 200 | (lookup(buffer(offset + 28).toInt) << 12) 201 | | (lookup(buffer(offset + 29).toInt) << 8) 202 | | (lookup(buffer(offset + 30).toInt) << 4) 203 | | (lookup(buffer(offset + 31).toInt) ) 204 | ) 205 | 206 | val lsb0 = ( 207 | (lookup(buffer(offset + 32).toInt) << 12) 208 | | (lookup(buffer(offset + 33).toInt) << 8) 209 | | (lookup(buffer(offset + 34).toInt) << 4) 210 | | (lookup(buffer(offset + 35).toInt) ) 211 | ) 212 | 213 | // check for separators and if any of the characters were not a hexadecimal value 214 | if (buffer(offset + 8) != '-' || buffer(offset + 13) != '-' || 215 | buffer(offset + 18) != '-' || buffer(offset + 23) != '-' || 216 | (msb3 | msb2 | msb1 | msb0 | lsb3 | lsb2 | lsb1 | lsb0) < 0) null else { 217 | UUID( 218 | (((msb3 << 16) | msb2).toLong << 32) | (msb1.toLong << 16) | msb0 219 | , (((lsb3 << 16) | lsb2).toLong << 32) | (lsb1.toLong << 16) | lsb0 220 | ) 221 | } 222 | } 223 | 224 | /** Strict parser proxy, should be inlined & optimized */ 225 | @inline private[this] final def fromStrictString(uuid: String): UUID = 226 | if (uuid.length == 36) { 227 | fromCharArrayViaLookup(uuid.toCharArray, 0, Lookup) 228 | } else { 229 | null 230 | } 231 | 232 | /** Creates a new `UUID` by parsing a `String` in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. */ 233 | final def apply(uuid: String): UUID = { 234 | val res = try { 235 | fromStrictString(uuid) 236 | } catch { 237 | case _: ArrayIndexOutOfBoundsException => null 238 | } 239 | if (res eq null) throw new IllegalArgumentException( 240 | "UUID must be in format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, where x is a hexadecimal digit (got: " + uuid + ")") 241 | res 242 | } 243 | 244 | /** Extractor which parses a `String` in `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` format. */ 245 | final def unapply(uuid: String): Option[UUID] = try { 246 | Option(fromStrictString(uuid)) 247 | } catch { 248 | case _: ArrayIndexOutOfBoundsException => None 249 | } 250 | 251 | /** Allows for parsing using the legacy, non-strict parser used in `java.util.UUID.fromString` */ 252 | @inline final def apply(uuid: String, strict: Boolean): UUID = 253 | if (strict) { 254 | apply(uuid) 255 | } else { 256 | fromString(uuid) 257 | } 258 | 259 | /** Generates a random `UUID` (type 4) and returns its lowercase String representation */ 260 | final def randomString: String = 261 | randomUUID().string // TODO: optimize this not to bother GC with throwaway UUIDs 262 | 263 | // --- Static forwarders --- 264 | 265 | /** Creates a new `UUID` by parsing a `String` in legacy (non-strict) format. */ 266 | @inline final def fromString(name: String): UUID = 267 | java.util.UUID.fromString(name) 268 | 269 | /** Generates a random `UUID` (type 4) using a cryptographically strong pseudo random number generator. */ 270 | @inline final def randomUUID(): UUID = 271 | java.util.UUID.randomUUID() 272 | 273 | /** Digests the provided byte array using MD5 and returns a type 3 (name based) `UUID`. */ 274 | @inline final def nameUUIDFromBytes(name: Array[Byte]): UUID = 275 | java.util.UUID.nameUUIDFromBytes(name) 276 | 277 | // --- Orderings --- 278 | 279 | /** Default JDK UUID ordering */ 280 | val signedOrdering: Ordering[UUID] = Ordering.fromLessThan((x, y) => (x compareTo y) == -1) 281 | 282 | /** Default scala-uuid ordering */ 283 | val unsignedOrdering: Ordering[UUID] = Ordering.fromLessThan(_ < _) 284 | } 285 | -------------------------------------------------------------------------------- /src/main/scala/io/jvm/uuid/package.scala: -------------------------------------------------------------------------------- 1 | package io.jvm 2 | 3 | /** This package holds the optimized Scala wrapper for `java.util.UUID`. 4 | * 5 | * To use the wrapper, either extend the [[uuid.Imports Imports]] trait 6 | * or import this package object to bring the implicit into scope that way: 7 | * {{{ 8 | * scala> import io.jvm.uuid._ 9 | * import io.jvm.uuid._ 10 | * 11 | * scala> val foo = classOf[UUID] 12 | * foo: Class[io.jvm.uuid.UUID] = class java.util.UUID 13 | * 14 | * scala> val bar = UUID(1, 2) 15 | * bar: io.jvm.uuid.UUID = 00000000-0000-0001-0000-000000000002 16 | * 17 | * scala> val UUID(baz) = "00112233-4455-6677-8899-aAbBcCdDeEfF" 18 | * baz: io.jvm.uuid.UUID = 00112233-4455-6677-8899-aabbccddeeff 19 | * }}} */ 20 | package object uuid extends Imports 21 | -------------------------------------------------------------------------------- /src/test/scala/test/io/jvm/uuid/ImportFeatureSpec.scala: -------------------------------------------------------------------------------- 1 | package test.io.jvm.uuid 2 | 3 | class ImportFeatureSpec 4 | extends org.specs2.Specification { 5 | 6 | def is = s2""" 7 | Importing the functionality of the `io.jvm.uuid` package object 8 | type alias ${ViaPackageObjectImport.typeAlias} 9 | static forwarder ${ViaPackageObjectImport.staticForwarder} 10 | rich functionality ${ViaPackageObjectImport.richFunctionality} 11 | ordering ${ViaPackageObjectImport.ordering} 12 | 13 | Extending the trait `io.jvm.uuid.Imports` 14 | type alias ${ViaExtendingImportsTrait.typeAlias} 15 | static forwarder ${ViaExtendingImportsTrait.staticForwarder} 16 | rich functionality ${ViaExtendingImportsTrait.richFunctionality} 17 | ordering ${ViaExtendingImportsTrait.ordering} 18 | """ 19 | 20 | object ViaPackageObjectImport { 21 | import io.jvm.uuid._ 22 | 23 | def typeAlias = 24 | classOf[UUID] ==== classOf[java.util.UUID] 25 | 26 | def staticForwarder = 27 | UUID.fromString("1-2-3-4-5") ==== java.util.UUID.fromString("1-2-3-4-5") 28 | 29 | def richFunctionality = 30 | UUID(Array.fill(16)(0xff.toByte)) ==== new java.util.UUID(~0L, ~0L) 31 | 32 | def ordering = 33 | (UUID(-1, 0) > UUID(1, 0)) ==== true 34 | } 35 | 36 | object ViaExtendingImportsTrait 37 | extends io.jvm.uuid.Imports { 38 | 39 | def typeAlias = 40 | classOf[UUID] ==== classOf[java.util.UUID] 41 | 42 | def staticForwarder = 43 | UUID.fromString("1-2-3-4-5") ==== java.util.UUID.fromString("1-2-3-4-5") 44 | 45 | def richFunctionality = 46 | UUID(Array.fill(16)(0xff.toByte)) ==== new java.util.UUID(~0L, ~0L) 47 | 48 | def ordering = 49 | (UUID(-1, 0) > UUID(1, 0)) ==== true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/scala/test/io/jvm/uuid/UUIDFeatureCheck.scala: -------------------------------------------------------------------------------- 1 | package test.io.jvm.uuid 2 | 3 | import java.security.MessageDigest 4 | import java.util.Locale 5 | 6 | import io.jvm.uuid._ 7 | import org.scalacheck._ 8 | import org.specs2._ 9 | 10 | import scala.util._ 11 | 12 | class UUIDFeatureCheck 13 | extends Specification 14 | with ScalaCheck { 15 | 16 | def is = s2""" 17 | Long roundtrips 18 | msb, lsb longs $msbLsbLongs 19 | 20 | String roundtrips 21 | strict $strictString 22 | non-strict (fail) $nonStrictString 23 | strict with check $strictStringWithCheck 24 | non-strict with check (fail) $invalidStrictStringFailure 25 | unapply with invalid (fail) $unapplyWithInvalidStringFailure 26 | non-strict with check $nonStrictStringWithCheck 27 | apply with suffix (fail) $applyWithSuffixFailure 28 | unapply with suffix (fail) $unapplyWithSuffixFailure 29 | 30 | Array roundtrips 31 | long array $longArray 32 | long array wrong size $longArrayWrongSize 33 | long array with offset $longArrayWithOffset 34 | long array with offsets $longArrayWithOffsets 35 | 36 | int array $intArray 37 | int array wrong size $intArrayWrongSize 38 | int array with offset $intArrayWithOffset 39 | int array with offsets $intArrayWithOffsets 40 | 41 | short array $shortArray 42 | short array wrong size $shortArrayWrongSize 43 | short array with offset $shortArrayWithOffset 44 | short array with offsets $shortArrayWithOffsets 45 | 46 | byte array $byteArray 47 | byte array wrong size $byteArrayWrongSize 48 | byte array with offset $byteArrayWithOffset 49 | byte array with offsets $byteArrayWithOffsets 50 | 51 | char array $charArray 52 | char array (fail) $charArrayFailure 53 | char array wrong size $charArrayWrongSize 54 | char array with offset $charArrayWithOffset 55 | char array with offsets $charArrayWithOffsets 56 | 57 | randomString $randomString 58 | 59 | Version conformism 60 | naming is version 3 $namingIsVersion3 61 | random is version 4 $randomIsVersion4 62 | 63 | Comparison 64 | Signed via legacy $signedComparison 65 | Unsigned by default $unsignedComparison 66 | """ 67 | 68 | // Long roundtrips 69 | 70 | def msbLsbLongs = prop { (msb: Long, lsb: Long) => 71 | val uuid = UUID(msb, lsb) 72 | msb ==== uuid.getMostSignificantBits && 73 | msb ==== uuid.mostSigBits && 74 | lsb ==== uuid.getLeastSignificantBits && 75 | lsb ==== uuid.leastSigBits 76 | } 77 | 78 | // String roundtrips 79 | 80 | private val hexValueGen = Gen.choose(0, 16) 81 | private val hexLowerGen = hexValueGen.map(v => "%x".format(v).head) 82 | private val hexUpperGen = hexLowerGen.map(_.toUpper) 83 | private val hexCharGen = Gen.oneOf(hexLowerGen, hexUpperGen) 84 | 85 | private def fixedHexWordGen(length: Int) = Gen.listOfN(length, hexCharGen).map(_.mkString) 86 | private def variableHexWordGen(minLength: Int, maxLength: Int) = fixedHexWordGen(maxLength).map(_ take minLength) 87 | 88 | private implicit class RichString(underlying: String) { 89 | def untrim(length: Int) = (("0" * length) + underlying) takeRight length 90 | } 91 | 92 | private val strictStringGen = for { 93 | w0 <- fixedHexWordGen(8) 94 | w1 <- fixedHexWordGen(4) 95 | w2 <- fixedHexWordGen(4) 96 | w3 <- fixedHexWordGen(4) 97 | w4 <- fixedHexWordGen(12) 98 | } yield s"$w0-$w1-$w2-$w3-$w4" 99 | 100 | private val nonStrictStringGen = for { 101 | w0 <- variableHexWordGen(1, 8) 102 | w1 <- variableHexWordGen(1, 4) 103 | w2 <- variableHexWordGen(1, 4) 104 | w3 <- variableHexWordGen(1, 4) 105 | w4 <- variableHexWordGen(1, 12) 106 | } yield s"$w0-$w1-$w2-$w3-$w4" 107 | 108 | def strictString = Prop.forAllNoShrink(strictStringGen) { ss => 109 | UUID(ss ).string ==== ss.toLowerCase(Locale.ROOT) && 110 | UUID(ss, false).string ==== ss.toLowerCase(Locale.ROOT) 111 | UUID(ss ).toLowerCase ==== ss.toLowerCase(Locale.ROOT) && 112 | UUID(ss, false).toLowerCase ==== ss.toLowerCase(Locale.ROOT) 113 | UUID(ss ).toUpperCase ==== ss.toUpperCase(Locale.ROOT) && 114 | UUID(ss, false).toUpperCase ==== ss.toUpperCase(Locale.ROOT) 115 | } 116 | 117 | def nonStrictString = Prop.forAllNoShrink(nonStrictStringGen) { nss => 118 | Try { UUID(nss ) }.isFailure && 119 | Try { UUID(nss, true) }.isFailure 120 | } 121 | 122 | def strictStringWithCheck = Prop.forAllNoShrink(strictStringGen) { ss => 123 | UUID(ss, true).string ==== ss.toLowerCase(Locale.ROOT) 124 | } 125 | 126 | private val placeInStrictStringGen = Gen.choose(0, UUID.random.string.length - 1) 127 | private val nonHexUpperCharGen = Gen.choose('G', 'Z') 128 | private val nonHexLowerCharGen = nonHexUpperCharGen.map(_.toLower) 129 | private val nonHexCharGen = Gen.oneOf(nonHexUpperCharGen, nonHexLowerCharGen) 130 | 131 | private val invalidStrictStringGen = for { 132 | ss <- strictStringGen 133 | index <- placeInStrictStringGen 134 | lower <- nonHexCharGen 135 | } yield ss.updated(index, lower) 136 | 137 | def invalidStrictStringFailure = Prop.forAllNoShrink(invalidStrictStringGen) { iss => 138 | UUID(iss, true) must throwA(new IllegalArgumentException("UUID must be in format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, where x is a hexadecimal digit (got: " + iss + ")")) 139 | } 140 | 141 | def unapplyWithInvalidStringFailure = Prop.forAllNoShrink(invalidStrictStringGen) { iss => 142 | UUID.unapply(iss) ==== None 143 | } 144 | 145 | def nonStrictStringWithCheck = Prop.forAllNoShrink(nonStrictStringGen) { nss => 146 | val Array(w0, w1, w2, w3, w4) = nss.split("-") 147 | val ss = s"${w0.untrim(8)}-${w1.untrim(4)}-${w2.untrim(4)}-${w3.untrim(4)}-${w4.untrim(12)}" 148 | val result = Try { UUID(nss, true).string } 149 | if (nss == ss) { 150 | result == Success(ss.toLowerCase(Locale.ROOT)) 151 | } else { 152 | result.isFailure 153 | } 154 | } 155 | 156 | private val hexOrNoHexGen = Gen.oneOf(hexCharGen, nonHexCharGen) 157 | private val strictStringWithSuffixGen = for { 158 | ss <- strictStringGen 159 | ch <- hexOrNoHexGen 160 | } yield ss + ch 161 | 162 | def applyWithSuffixFailure = Prop.forAllNoShrink(strictStringWithSuffixGen) { sss => 163 | UUID(sss) must throwA(new IllegalArgumentException("UUID must be in format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, where x is a hexadecimal digit (got: " + sss + ")")) 164 | } 165 | 166 | def unapplyWithSuffixFailure = Prop.forAllNoShrink(strictStringWithSuffixGen) { sss => 167 | UUID.unapply(sss) ==== None 168 | } 169 | 170 | // Array roundtrips 171 | 172 | private val longGen = Arbitrary.arbLong.arbitrary 173 | private val longArrayGen = Gen.listOfN[Long](2, longGen).map(_.toArray) 174 | private val longArrayTooShortGen = Gen.listOfN[Long](1, longGen).map(_.toArray) 175 | private val longArrayTooLongGen = Gen.listOfN[Long](4, longGen).map(_.toArray) 176 | 177 | def longArray = Prop.forAllNoShrink(longArrayGen) { la => 178 | UUID(la).longArray ==== la 179 | } 180 | 181 | def longArrayWrongSize = Prop.forAllNoShrink(longArrayTooShortGen, longArrayTooLongGen) { (tooShort, tooLong) => 182 | Try { UUID(tooShort) }.isFailure && 183 | Try { UUID(tooLong ) }.isFailure 184 | } 185 | 186 | def longArrayWithOffset = Prop.forAllNoShrink(longArrayGen, longArrayTooLongGen) { (src, dst) => 187 | UUID(src).toLongArray(dst, 1) 188 | dst.tail.init ==== src 189 | } 190 | 191 | def longArrayWithOffsets = Prop.forAllNoShrink(longArrayTooLongGen, longArrayTooLongGen) { (src, dst) => 192 | UUID.fromLongArray(src, 1).toLongArray(dst, 1) 193 | dst.tail.init ==== src.tail.init 194 | } 195 | 196 | private val intGen = Arbitrary.arbInt.arbitrary 197 | private val intArrayGen = Gen.listOfN[Int](4, intGen).map(_.toArray) 198 | private val intArrayTooShortGen = Gen.listOfN[Int](3, intGen).map(_.toArray) 199 | private val intArrayTooLongGen = Gen.listOfN[Int](6, intGen).map(_.toArray) 200 | 201 | def intArray = Prop.forAllNoShrink(intArrayGen) { ia => 202 | UUID(ia).intArray ==== ia 203 | } 204 | 205 | def intArrayWrongSize = Prop.forAllNoShrink(intArrayTooShortGen, intArrayTooLongGen) { (tooShort, tooLong) => 206 | Try { UUID(tooShort) }.isFailure && 207 | Try { UUID(tooLong ) }.isFailure 208 | } 209 | 210 | def intArrayWithOffset = Prop.forAllNoShrink(intArrayGen, intArrayTooLongGen) { (src, dst) => 211 | UUID(src).toIntArray(dst, 1) 212 | dst.tail.init ==== src 213 | } 214 | 215 | def intArrayWithOffsets = Prop.forAllNoShrink(intArrayTooLongGen, intArrayTooLongGen) { (src, dst) => 216 | UUID.fromIntArray(src, 1).toIntArray(dst, 1) 217 | dst.tail.init ==== src.tail.init 218 | } 219 | 220 | private val shortGen = Arbitrary.arbShort.arbitrary 221 | private val shortArrayGen = Gen.listOfN[Short](8, shortGen).map(_.toArray) 222 | private val shortArrayTooShortGen = Gen.listOfN[Short](7, shortGen).map(_.toArray) 223 | private val shortArrayTooLongGen = Gen.listOfN[Short](10, shortGen).map(_.toArray) 224 | 225 | def shortArray = Prop.forAllNoShrink(shortArrayGen) { sa => 226 | UUID(sa).shortArray ==== sa 227 | } 228 | 229 | def shortArrayWrongSize = Prop.forAllNoShrink(shortArrayTooShortGen, shortArrayTooLongGen) { (tooShort, tooLong) => 230 | Try { UUID(tooShort) }.isFailure && 231 | Try { UUID(tooLong ) }.isFailure 232 | } 233 | 234 | def shortArrayWithOffset = Prop.forAllNoShrink(shortArrayGen, shortArrayTooLongGen) { (src, dst) => 235 | UUID(src).toShortArray(dst, 1) 236 | dst.tail.init ==== src 237 | } 238 | 239 | def shortArrayWithOffsets = Prop.forAllNoShrink(shortArrayTooLongGen, shortArrayTooLongGen) { (src, dst) => 240 | UUID.fromShortArray(src, 1).toShortArray(dst, 1) 241 | dst.tail.init ==== src.tail.init 242 | } 243 | 244 | private val byteGen = Arbitrary.arbByte.arbitrary 245 | private val byteArrayGen = Gen.listOfN[Byte](16, byteGen).map(_.toArray) 246 | private val byteArrayTooShortGen = Gen.listOfN[Byte](15, byteGen).map(_.toArray) 247 | private val byteArrayTooLongGen = Gen.listOfN[Byte](18, byteGen).map(_.toArray) 248 | 249 | def byteArray = Prop.forAllNoShrink(byteArrayGen) { ba => 250 | UUID(ba).byteArray ==== ba 251 | } 252 | 253 | def byteArrayWrongSize = Prop.forAllNoShrink(byteArrayTooShortGen, byteArrayTooLongGen) { (tooShort, tooLong) => 254 | Try { UUID(tooShort) }.isFailure && 255 | Try { UUID(tooLong ) }.isFailure 256 | } 257 | 258 | def byteArrayWithOffset = Prop.forAllNoShrink(byteArrayGen, byteArrayTooLongGen) { (src, dst) => 259 | UUID(src).toByteArray(dst, 1) 260 | dst.tail.init ==== src 261 | } 262 | 263 | def byteArrayWithOffsets = Prop.forAllNoShrink(byteArrayTooLongGen, byteArrayTooLongGen) { (src, dst) => 264 | UUID.fromByteArray(src, 1).toByteArray(dst, 1) 265 | dst.tail.init ==== src.tail.init 266 | } 267 | 268 | private val charArrayGen = strictStringGen.map(_.toCharArray) 269 | private val charArrayTooShortGen = charArrayGen.map(_.init) 270 | private val charArrayTooLongGen = charArrayGen.map(ca => '?' +: ca :+ '?') 271 | private val charArrayInvalidGen = invalidStrictStringGen.map(_.toCharArray) 272 | 273 | def charArray = Prop.forAllNoShrink(charArrayGen) { ca => 274 | UUID(ca).charArray ==== ca.map(_.toLower) 275 | } 276 | 277 | def charArrayFailure = Prop.forAllNoShrink(charArrayInvalidGen) { ca => 278 | Try { UUID(ca).charArray }.isFailure 279 | } 280 | 281 | def charArrayWrongSize = Prop.forAllNoShrink(charArrayTooShortGen, charArrayTooLongGen) { (tooShort, tooLong) => 282 | Try { UUID(tooShort) }.isFailure && 283 | Try { UUID(tooLong ) }.isFailure 284 | } 285 | 286 | def charArrayWithOffset = Prop.forAllNoShrink(charArrayGen, charArrayTooLongGen) { (src, dst) => 287 | UUID(src).toCharArray(dst, 1) 288 | dst.tail.init ==== src.map(_.toLower) 289 | } 290 | 291 | def charArrayWithOffsets = Prop.forAllNoShrink(charArrayTooLongGen, charArrayTooLongGen) { (src, dst) => 292 | UUID.fromCharArray(src, 1).toCharArray(dst, 1) 293 | dst.tail.init ==== src.tail.init.map(_.toLower) 294 | } 295 | 296 | def randomString = Prop.forAll(()) { _ => 297 | val uuidString = UUID.randomString 298 | uuidString ==== uuidString.toLowerCase(Locale.ROOT) && 299 | UUID(uuidString).string ==== uuidString 300 | } 301 | 302 | // Version conformism 303 | 304 | /** Naming conforms to version 3 spec: 305 | * xxxxxxxx-xxxx-3xxx-yxxx-xxxxxxxxxxxx where x is any hexadecimal digit and y is one of 8, 9, A, or B */ 306 | def namingIsVersion3 = Prop.forAllNoShrink(byteArrayGen) { ba => 307 | val uuid = UUID.nameUUIDFromBytes(ba) 308 | val version = (uuid.getMostSignificantBits >>> 12) & 0xF 309 | val variant = uuid.getLeastSignificantBits >>> 62 310 | version ==== 3 && variant ==== 2 && { 311 | val reconstruct = UUID(MessageDigest.getInstance("MD5").digest(ba)) 312 | (reconstruct.mostSigBits & ~0xF000L) ==== (uuid.mostSigBits & ~0xF000L) && 313 | (reconstruct.leastSigBits & ~0xC000000000000000L) ==== (uuid.leastSigBits & ~0xC000000000000000L) // cool :) 314 | } 315 | } 316 | 317 | /** Random conforms to version 4 spec: 318 | * xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx where x is any hexadecimal digit and y is one of 8, 9, A, or B */ 319 | def randomIsVersion4 = Prop.forAll(()) { _ => 320 | val uuid = UUID.random 321 | val version = (uuid.getMostSignificantBits >>> 12) & 0xF 322 | val variant = uuid.getLeastSignificantBits >>> 62 323 | version ==== 4 && variant ==== 2 324 | } 325 | 326 | // Comparisons 327 | 328 | def signedComparison = prop { (msb1: Long, lsb1: Long, msb2: Long, lsb2: Long) => 329 | val uuid1 = UUID(msb1, lsb1) 330 | val uuid2 = UUID(msb2, lsb2) 331 | (uuid1 compareTo uuid2) ==== { 332 | val msb = java.lang.Long.compare(uuid1.mostSigBits, uuid2.mostSigBits) 333 | val lsb = java.lang.Long.compare(uuid1.leastSigBits, uuid2.leastSigBits) 334 | if (msb != 0) msb else lsb 335 | } 336 | } 337 | 338 | def unsignedComparison = prop { (msb1: Long, lsb1: Long, msb2: Long, lsb2: Long) => 339 | val uuid1 = UUID(msb1, lsb1) 340 | val uuid2 = UUID(msb2, lsb2) 341 | (uuid1 compare uuid2) ==== { 342 | val msb = java.lang.Long.compareUnsigned(uuid1.mostSigBits, uuid2.mostSigBits) 343 | val lsb = java.lang.Long.compareUnsigned(uuid1.leastSigBits, uuid2.leastSigBits) 344 | if (msb != 0) msb else lsb 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/test/scala/test/io/jvm/uuid/UUIDFeatureSpec.scala: -------------------------------------------------------------------------------- 1 | package test.io.jvm.uuid 2 | 3 | import io.jvm.uuid._ 4 | 5 | import scala.util.Try 6 | 7 | class UUIDFeatureSpec 8 | extends org.specs2.Specification { 9 | 10 | def is = s2""" 11 | Primary constructor 12 | from two longs $fromTwoLongs 13 | 14 | String constructors 15 | from strict $fromStrictString 16 | from non-strict (fail) $fromNonStrictString 17 | from strict $fromStrictStringWithCheck 18 | from non-strict with check (fail) $fromNonStrictStringWithCheck 19 | from strict without check $fromStrictStringWithoutCheck 20 | from non-strict without check $fromNonStrictStringWithoutCheck 21 | 22 | Array constructors 23 | from long array $fromLongArray 24 | from int array $fromIntArray 25 | from short array $fromShortArray 26 | from byte array $fromByteArray 27 | 28 | Accessors 29 | to msb, lsb longs $longAccessors 30 | to long array $toLongArray 31 | to int array $toIntArray 32 | to short array $toShortArray 33 | to byte array $toByteArray 34 | to char array $toCharArray 35 | to string $toDefaultCaseString 36 | to lower-case string $toLowerCaseString 37 | to upper-case string $toUpperCaseString 38 | 39 | Extractors 40 | uuid extractor $uuidExtractor 41 | uuid extractor (failure) $uuidExtractorFailure 42 | 43 | Comparisons 44 | signed comparison $signedComparsion 45 | signed comparison (via ordering) $explictSignedComparsion 46 | unsigned comparison by default $unsignedComparsion 47 | unsigned comparison (via ordering) $explicitUnsignedComparsion 48 | equal comparison $equalComparsion 49 | """ 50 | 51 | private val u159bf = java.util.UUID.fromString("1-5-9-b-f") 52 | 53 | // Primary constructor 54 | 55 | def fromTwoLongs = 56 | UUID(0x100050009L, 0xb00000000000fL) ==== u159bf 57 | 58 | // String constructors 59 | 60 | def fromStrictString = 61 | UUID("00000001-0005-0009-000b-00000000000f") ==== u159bf 62 | 63 | def fromNonStrictString = 64 | Try { UUID("1-5-9-b-f") ==== u159bf } isFailure 65 | 66 | def fromStrictStringWithCheck = 67 | UUID("00000001-0005-0009-000b-00000000000f", true) ==== u159bf 68 | 69 | def fromNonStrictStringWithCheck = 70 | Try { UUID("1-5-9-b-f", true) } isFailure 71 | 72 | def fromStrictStringWithoutCheck = 73 | UUID("00000001-0005-0009-000b-00000000000f", false) ==== u159bf 74 | 75 | def fromNonStrictStringWithoutCheck = 76 | UUID("1-5-9-b-f", false) ==== u159bf 77 | 78 | // Array constructors 79 | 80 | def fromLongArray = 81 | UUID(Array(0x100050009L, 0xb00000000000fL)) ==== u159bf 82 | 83 | def fromIntArray = 84 | UUID(Array(1, 0x50009, 0xb0000, 0xf)) ==== u159bf 85 | 86 | def fromShortArray = 87 | UUID(Array[Short](0, 1, 5, 9, 0xb, 0, 0, 0xf)) ==== u159bf 88 | 89 | def fromByteArray = 90 | UUID(Array[Byte](0, 0, 0, 1, 0, 5, 0, 9, 0, 0xb, 0, 0, 0, 0, 0, 0xf)) ==== u159bf 91 | 92 | def fromCharArray = 93 | UUID("00000001-0005-0009-000b-00000000000f".toCharArray) ==== u159bf 94 | 95 | // Accessors 96 | 97 | def longAccessors = 98 | u159bf.mostSigBits ==== 0x100050009L && 99 | u159bf.leastSigBits ==== 0xb00000000000fL 100 | 101 | def toLongArray = 102 | u159bf.longArray ==== Array(0x100050009L, 0xb00000000000fL) 103 | 104 | def toIntArray = 105 | u159bf.intArray ==== Array(1, 0x50009, 0xb0000, 0xf) 106 | 107 | def toShortArray = 108 | u159bf.shortArray ==== Array[Short](0, 1, 5, 9, 0xb, 0, 0, 0xf) 109 | 110 | def toByteArray = 111 | u159bf.byteArray ==== Array[Byte](0, 0, 0, 1, 0, 5, 0, 9, 0, 0xb, 0, 0, 0, 0, 0, 0xf) 112 | 113 | def toCharArray = 114 | u159bf.charArray ==== "00000001-0005-0009-000b-00000000000f".toCharArray 115 | 116 | def toDefaultCaseString = 117 | u159bf.string ==== "00000001-0005-0009-000b-00000000000f" 118 | 119 | def toLowerCaseString = 120 | u159bf.toLowerCase ==== "00000001-0005-0009-000b-00000000000f" 121 | 122 | def toUpperCaseString = 123 | u159bf.toUpperCase ==== "00000001-0005-0009-000B-00000000000F" 124 | 125 | // -- Extractors -- 126 | 127 | def uuidExtractor = { 128 | val UUID(uuidValue) = "00000001-0005-0009-000b-00000000000f" // will fail if not strict! 129 | uuidValue ==== u159bf 130 | } 131 | 132 | def uuidExtractorFailure = { 133 | "1-5-9-b-f" match { 134 | case UUID(_) => sys.error("Should not match - it wasn't strict!") 135 | case _ => true 136 | } 137 | } 138 | 139 | // -- Comparison -- 140 | 141 | def signedComparsion = 142 | (UUID("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") compareTo u159bf) ==== -1 143 | 144 | def explictSignedComparsion = 145 | UUID.signedOrdering.compare(UUID("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"), u159bf) ==== -1 146 | 147 | def unsignedComparsion = 148 | (UUID("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") compare u159bf) ==== 1 149 | 150 | def explicitUnsignedComparsion = 151 | UUID.unsignedOrdering.compare(UUID("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"), u159bf) ==== 1 152 | 153 | def equalComparsion = 154 | (u159bf compare u159bf) ==== 0 155 | } 156 | --------------------------------------------------------------------------------