├── example └── main.dart ├── analysis_options.yaml ├── pubspec.yaml ├── .gitignore ├── README.md ├── CHANGELOG.md ├── LICENSE ├── test └── ulid_test.dart └── lib └── ulid.dart /example/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Agilord. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:ulid/ulid.dart'; 5 | 6 | void main() { 7 | print(Ulid()); 8 | print(Ulid().toUuid()); 9 | } 10 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | # exclude: 3 | # - path/to/excluded/files/** 4 | 5 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 6 | linter: 7 | rules: 8 | - cancel_subscriptions 9 | - close_sinks 10 | - hash_and_equals 11 | - test_types_in_equals 12 | - unrelated_type_equality_checks 13 | - valid_regexps 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ulid 2 | description: > 3 | Lexicographically sortable, 128-bit identifier (UUID) with 48-bit timestamp and 80 random bits. 4 | Canonically encoded as a 26 character string, as opposed to the 36 character UUID. 5 | version: 2.0.1 6 | homepage: https://github.com/agilord/ulid 7 | 8 | environment: 9 | sdk: '^3.0.0' 10 | 11 | dev_dependencies: 12 | lints: ^4.0.0 13 | test: ^1.0.0 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .packages 3 | .pub/ 4 | .dart_tool/ 5 | build/ 6 | packages 7 | # Remove the following pattern if you wish to check in your lock file 8 | pubspec.lock 9 | 10 | # Files created by dart2js 11 | *.dart.js 12 | *.part.js 13 | *.js.deps 14 | *.js.map 15 | *.info.json 16 | 17 | # Directory created by dartdoc 18 | doc/api/ 19 | 20 | # JetBrains IDEs 21 | .idea/ 22 | *.iml 23 | *.ipr 24 | *.iws 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ulid implementation in Dart 2 | 3 | Lexicographically sortable, 128-bit identifier (UUID) with 48-bit timestamp and 80 random bits. 4 | Canonically encoded as a 26 character string, as opposed to the 36 character UUID. 5 | 6 | Original implementation: https://github.com/alizain/ulid/ 7 | 8 | ## Usage 9 | 10 | A simple usage example: 11 | 12 | ````dart 13 | import 'package:ulid/ulid.dart'; 14 | 15 | main() { 16 | print(Ulid()); 17 | print(Ulid().toUuid()); 18 | } 19 | ```` 20 | 21 | ## Links 22 | 23 | - [source code][source] 24 | - contributors: [Agilord][agilord] 25 | 26 | [source]: https://github.com/agilord/ulid 27 | [agilord]: https://www.agilord.com/ 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.1 4 | 5 | - Updated lints. 6 | 7 | ## 2.0.0 8 | 9 | - Migrate to null safety. ([#10](https://github.com/agilord/ulid/pull/10) by [ChristianGaertner](https://github.com/ChristianGaertner)) 10 | 11 | ## 1.1.0 12 | 13 | - Updated code to 2.7 Dart and latest `pedantic` lints. 14 | - Added `Ulid.toBytes` and `Ulid.fromBytes`. 15 | 16 | ## 1.0.4 17 | 18 | - implement `operator ==` and `hashCode` 19 | 20 | ## 1.0.3 21 | 22 | - Fixed bit loss in browser's int handling. 23 | 24 | ## 1.0.2 25 | 26 | - Using `package:pedantic`. 27 | - Removed leftover `main` method. 28 | 29 | ## 1.0.1 30 | 31 | - Declared support for Dart 2. 32 | - Upgraded dependencies. 33 | 34 | ## 1.0.0 35 | 36 | - First public version. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Agilord. 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 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /test/ulid_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Agilord. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:ulid/ulid.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('length', () { 9 | final id = Ulid(); 10 | expect(id.toCanonical(), hasLength(26)); 11 | expect(id.toUuid(), hasLength(36)); 12 | expect(id.toUuid(compact: true), hasLength(32)); 13 | }); 14 | 15 | test('fixed time', () { 16 | final id = Ulid(millis: 1469918176385); 17 | expect(id.toCanonical().substring(0, 10), '01aryz6s41'); 18 | expect(id.toMillis(), 1469918176385); 19 | }); 20 | 21 | test('parse compact', () { 22 | final id = Ulid.parse('01bj755t69g1r3e2c7fseyb102'); 23 | expect(id.toCanonical(), '01bj755t69g1r3e2c7fseyb102'); 24 | expect(id.toUuid(), '015c8e52-e8c9-8070-3709-877e5de58402'); 25 | expect(id.toMillis(), 1497036417225); 26 | }); 27 | 28 | test('parse uuid', () { 29 | final id = Ulid.parse('015c8e52-e8c9-8070-3709-877e5de58402'); 30 | expect(id.toCanonical(), '01bj755t69g1r3e2c7fseyb102'); 31 | expect(id.toUuid(), '015c8e52-e8c9-8070-3709-877e5de58402'); 32 | expect(id.toMillis(), 1497036417225); 33 | }); 34 | 35 | test('parse bytes', () { 36 | final bytes = [ 37 | 1, 38 | 92, 39 | 142, 40 | 82, 41 | 232, 42 | 201, 43 | 128, 44 | 112, 45 | 55, 46 | 9, 47 | 135, 48 | 126, 49 | 93, 50 | 229, 51 | 132, 52 | 2, 53 | ]; 54 | final id = Ulid.fromBytes(bytes); 55 | expect(id.toCanonical(), '01bj755t69g1r3e2c7fseyb102'); 56 | expect(id.toUuid(), '015c8e52-e8c9-8070-3709-877e5de58402'); 57 | expect(id.toMillis(), 1497036417225); 58 | expect(id.toBytes(), bytes); 59 | }); 60 | 61 | test('operator ==', () { 62 | final ulid1 = Ulid(); 63 | final ulid2 = Ulid.parse(ulid1.toCanonical()); 64 | expect(ulid2, ulid1); 65 | expect(ulid1, isNot(Ulid())); 66 | }); 67 | 68 | test('hashCode', () { 69 | final ulid1 = Ulid(); 70 | final ulid2 = Ulid.parse(ulid1.toCanonical()); 71 | 72 | expect(ulid2.hashCode, ulid1.hashCode); 73 | expect(ulid1, isNot(Ulid().hashCode)); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /lib/ulid.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Agilord. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | /// Lexicographically sortable, 128-bit identifier (UUID) with 48-bit timestamp 5 | /// and 80 random bits. Canonically encoded as a 26 character string. 6 | /// 7 | /// Original implementation: https://github.com/alizain/ulid/ 8 | library ulid; 9 | 10 | import 'dart:math'; 11 | import 'dart:typed_data'; 12 | 13 | Random _random = Random.secure(); 14 | 15 | /// Lexicographically sortable, 128-bit identifier (UUID) with 48-bit timestamp 16 | /// and 80 random bits. Canonically encoded as a 26 character string, as opposed 17 | /// to the 36 character UUID. 18 | class Ulid { 19 | final Uint8List _data; 20 | 21 | Ulid._(this._data) { 22 | assert(_data.length == 16); 23 | } 24 | 25 | /// Create a [Ulid] instance. 26 | factory Ulid({int? millis}) { 27 | final data = Uint8List(16); 28 | var ts = millis ?? DateTime.now().millisecondsSinceEpoch; 29 | for (var i = 5; i >= 0; i--) { 30 | data[i] = ts & 0xFF; 31 | ts = ts >> 8; 32 | } 33 | for (var i = 6; i < 16; i++) { 34 | data[i] = _random.nextInt(256); 35 | } 36 | return Ulid._(data); 37 | } 38 | 39 | /// Parse the canonical or the UUID format. 40 | factory Ulid.parse(String value) { 41 | if (value.length == 26) { 42 | return Ulid._parseBase32(value); 43 | } else if (value.length == 32) { 44 | return Ulid._parseHex16(value); 45 | } else if (value.length == 36) { 46 | // TODO: assert dash positions 47 | final withoutSlashes = value.replaceAll('-', ''); 48 | if (withoutSlashes.length == 32) return Ulid._parseHex16(withoutSlashes); 49 | } 50 | throw ArgumentError('Unable to recognize format: $value'); 51 | } 52 | 53 | /// Creates a new instance form the provided bytes buffer. 54 | factory Ulid.fromBytes(List bytes) { 55 | if (bytes.length != 16 || bytes.any((b) => b > 256 || b < 0)) { 56 | throw ArgumentError.value(bytes, 'bytes', 'Invalid input.'); 57 | } 58 | return Ulid._(Uint8List.fromList(bytes)); 59 | } 60 | 61 | factory Ulid._parseBase32(String value) { 62 | final lc = value.toLowerCase(); 63 | final data = Uint8List(16); 64 | final buffer = Uint8List(26); 65 | for (var i = 0; i < 26; i++) { 66 | buffer[i] = _base32Decode[lc.codeUnitAt(i)]; 67 | } 68 | _decode(buffer, 0, 9, data, 0, 5); // time 69 | _decode(buffer, 10, 17, data, 6, 10); // random higher 40 bit 70 | _decode(buffer, 18, 25, data, 11, 15); // random lower 40 bit 71 | return Ulid._(data); 72 | } 73 | 74 | factory Ulid._parseHex16(String value) { 75 | final data = Uint8List(16); 76 | for (var i = 0; i < 16; i++) { 77 | data[i] = int.parse(value.substring(i * 2, i * 2 + 2), radix: 16); 78 | } 79 | return Ulid._(data); 80 | } 81 | 82 | /// Render the 36- or 32-character UUID format. 83 | String toUuid({bool compact = false}) { 84 | final sb = StringBuffer(); 85 | for (var i = 0; i < 16; i++) { 86 | if (!compact && (i == 4 || i == 6 || i == 8 || i == 10)) { 87 | sb.write('-'); 88 | } 89 | sb.write(_hex[_data[i] >> 4]); 90 | sb.write(_hex[_data[i] & 0x0F]); 91 | } 92 | return sb.toString(); 93 | } 94 | 95 | /// Render the canonical, 26-character format. 96 | String toCanonical() { 97 | final result = Uint8List(26); 98 | _encode(0, 5, result, 0, 9); // time 99 | _encode(6, 10, result, 10, 17); // random upper 40-bit 100 | _encode(11, 15, result, 18, 25); // random lower 40-bit 101 | final sb = StringBuffer(); 102 | for (var i = 0; i < 26; i++) { 103 | sb.write(_base32[result[i]]); 104 | } 105 | return sb.toString(); 106 | } 107 | 108 | /// Get the millisecond component. 109 | int toMillis() { 110 | var millis = 0; 111 | for (var i = 0; i < 6; i++) { 112 | millis = (millis << 8) + _data[i]; 113 | } 114 | return millis; 115 | } 116 | 117 | /// Get the internals as bytes (copied buffer). 118 | Uint8List toBytes() { 119 | return Uint8List.fromList(_data); 120 | } 121 | 122 | @override 123 | String toString() => toCanonical(); 124 | 125 | @override 126 | bool operator ==(other) { 127 | if (other is Ulid) { 128 | for (var i = 0; i < _data.length; i++) { 129 | if (other._data[i] != _data[i]) return false; 130 | } 131 | return true; 132 | } 133 | 134 | return false; 135 | } 136 | 137 | @override 138 | int get hashCode => _data.join().hashCode; 139 | 140 | void _encode(int inS, int inE, Uint8List buffer, int outS, int outE) { 141 | var value = BigInt.from(0); 142 | for (var i = inS; i <= inE; i++) { 143 | value = (value << 8) + BigInt.from(_data[i]); 144 | } 145 | for (var i = outE; i >= outS; i--) { 146 | buffer[i] = value.toInt() & 0x1F; 147 | value = value >> 5; 148 | } 149 | } 150 | 151 | static void _decode( 152 | Uint8List buffer, int inS, int inE, Uint8List data, int outS, int outE) { 153 | var value = BigInt.from(0); 154 | for (var i = inS; i <= inE; i++) { 155 | value = (value << 5) + BigInt.from(buffer[i]); 156 | } 157 | for (var i = outE; i >= outS; i--) { 158 | data[i] = value.toInt() & 0xFF; 159 | value = value >> 8; 160 | } 161 | } 162 | } 163 | 164 | // https://en.wikipedia.org/wiki/Base32 165 | String _hex16 = '0123456789abcdef'; 166 | String _crockfordBase32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'.toLowerCase(); 167 | 168 | List _hex = List.generate(16, (int i) => _hex16[i]); 169 | List _base32 = 170 | List.generate(32, (int i) => _crockfordBase32[i]); 171 | 172 | List _lowercaseCodes = 173 | List.generate(32, (int i) => _crockfordBase32[i].codeUnits.first); 174 | List _base32Decode = 175 | List.generate(256, (int i) => _lowercaseCodes.indexOf(i)); 176 | --------------------------------------------------------------------------------