Line data Source code
1 : // Copyright 2026 The Authors. See the AUTHORS file for details.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'dart:collection';
16 :
17 : import 'package:betto_common/string.dart' show StringExtension;
18 :
19 : import 'digit_string.dart' show DigitString;
20 :
21 : /// Digital Object Identifier (DOI).
22 : ///
23 : /// See: [DOI HANDBOOK](https://www.doi.org/the-identifier/resources/handbook/)
24 : class DOI {
25 : final DigitString directoryIndicator;
26 : final List<DigitString> _registrantCodes;
27 : final String suffix;
28 :
29 1 : DOI({
30 : required this.directoryIndicator,
31 : required List<DigitString> registrantCodes,
32 : required this.suffix,
33 1 : }) : _registrantCodes = [...registrantCodes];
34 :
35 1 : DOI._(this.directoryIndicator, this._registrantCodes, this.suffix);
36 :
37 1 : static DOI? tryParse(String value) {
38 : bool found;
39 : String prefix, suffix;
40 :
41 1 : (prefix, suffix, found) = value.cutFirst('/');
42 : if (!found) {
43 : return null;
44 : }
45 :
46 2 : if (prefix.isEmpty || suffix.isEmpty) {
47 : return null;
48 : }
49 :
50 : String directoryIndicatorStr, registrantCode;
51 1 : (directoryIndicatorStr, registrantCode, found) = prefix.cutFirst('.');
52 :
53 : if (!found) {
54 : return null;
55 : }
56 :
57 2 : if (directoryIndicatorStr.isEmpty || registrantCode.isEmpty) {
58 : return null;
59 : }
60 :
61 1 : final parseResult = DigitString.tryParse(directoryIndicatorStr);
62 :
63 : if (parseResult != null) {
64 : DigitString directoryIndicator = parseResult;
65 :
66 1 : final registrantCodes = _parseRegistrantCode(registrantCode);
67 1 : if (registrantCodes == null || registrantCodes.isEmpty) {
68 : return null;
69 : }
70 1 : final doi = DOI._(directoryIndicator, registrantCodes, suffix);
71 : return doi;
72 : }
73 : return null;
74 : }
75 :
76 1 : static List<DigitString>? _parseRegistrantCode(String registrantCode) {
77 1 : List<DigitString> registrantCodes = [];
78 :
79 2 : for (var digit in registrantCode.split('.')) {
80 1 : final digits = DigitString.tryParse(digit);
81 : if (digits == null) {
82 : return null;
83 : }
84 1 : registrantCodes.add(digits);
85 : }
86 : return registrantCodes;
87 : }
88 :
89 1 : UnmodifiableListView<DigitString> get registrantCodes =>
90 2 : UnmodifiableListView(_registrantCodes);
91 :
92 : // Uri get uri => Uri(scheme: 'https', host: 'doi.org', path: toString());
93 :
94 : /// DOI names are case insensitive.
95 : ///
96 : /// See: [DOI Handbook, Section 2.4](https://www.doi.org/the-identifier/resources/handbook/2_numbering#2.4)
97 1 : @override
98 : String toString() =>
99 6 : '$directoryIndicator.${_registrantCodes.join('.')}/$suffix'.toUpperCase();
100 :
101 : /// Uses https://doi.org
102 3 : Uri toUri() => Uri.https('doi.org', toString());
103 :
104 1 : @override
105 : bool operator ==(Object other) =>
106 4 : other is DOI && other.toString() == toString();
107 :
108 1 : @override
109 3 : int get hashCode => Object.hashAll([toString()]);
110 :
111 2 : static bool isValid(String value) => tryParse(value) != null;
112 : }
|