1 |
This is what an ideal and simple versioning spec should look like to |
2 |
me. (Not the form, but the concept). I'm posting this here so it |
3 |
could be used as an added reference to anyone that would consider |
4 |
revising the current specification. |
5 |
|
6 |
Note: Assigning default values can be bypassed depending on the implementation. |
7 |
|
8 |
Comments are welcome. |
9 |
---------------------------------------- |
10 |
1 Version Specification |
11 |
|
12 |
A version is composed of four component sets: the base part, the |
13 |
stage, the patch and the revision. |
14 |
|
15 |
Each component is composed mainly of version nodes to represent their |
16 |
level values. |
17 |
|
18 |
When a component is not specified, it gets a default node value of {0}. |
19 |
|
20 |
1.1 Version Nodes |
21 |
|
22 |
Nodes start basically with a number and then is optionally followed by |
23 |
a set of letters. |
24 |
|
25 |
Numbers and letters can coexist alternatingly to represent a single |
26 |
node. The number of consecutive letters is also not limited to 1. |
27 |
For example: "1a4xy" is allowed (can be restricted). |
28 |
|
29 |
Each set of digits represents a single decimal number. Leading zeros |
30 |
hold no meaning. |
31 |
|
32 |
The numerical equivalent of a set of letters is calculated in base of |
33 |
27 (0 exists along with it but is not included as a symbol). |
34 |
|
35 |
Version nodes after processing are basically just an array of signed |
36 |
integers where each set of digits or letters are converted to their |
37 |
numerical value. |
38 |
|
39 |
1.2 Version Parts |
40 |
|
41 |
1.2.1 The Base Part |
42 |
|
43 |
The base version is simply a set of version nodes that are separated |
44 |
by dots. Examples: "4.1.2", "4.1.2aa" "4a", "4a.1", "4a.1.2". |
45 |
|
46 |
After processing, the base version is an array of version nodes. |
47 |
|
48 |
It is required. |
49 |
|
50 |
1.2.2 The Stage Part |
51 |
|
52 |
The stage part starts with _alpha, _beta, _pre or _rc. Their |
53 |
numerical values are -4, -3, -2 or -1 respectively. Each of them can |
54 |
optionally be followed by a version node string. For example: |
55 |
"_alpha01". |
56 |
|
57 |
The resulting value for the stage after processing is a single version |
58 |
node where the first value in it is the numerical values of _alpha, |
59 |
_beta, _pre or _rc, and the other values are based on the added node |
60 |
string. |
61 |
|
62 |
A version without a stage has a default stage value of {0}. |
63 |
|
64 |
The stage part is optional and can be specified only once after the |
65 |
base part. It can't be specified as a modifier for the patch, for the |
66 |
revision, or for another stage. |
67 |
|
68 |
1.2.3 The Patch Part |
69 |
|
70 |
The patch part begins with _p and is followed by a version node |
71 |
string. For example: "_p20150105a". |
72 |
|
73 |
The patch part is optional and can be specified after the base part or |
74 |
after the stage part, but not after the revision. It can only be |
75 |
specified once. |
76 |
|
77 |
It is processed as a single version node based on its version node string. |
78 |
|
79 |
A version without a patch has a default patch value of {0}. |
80 |
|
81 |
1.2.4 The Revision |
82 |
|
83 |
The revision starts with -r and is followed by a number. |
84 |
|
85 |
It is processed as a single version node based on its version node string. |
86 |
|
87 |
A version without a revision has a default revision value of {0}. |
88 |
|
89 |
1.3 Comparing Versions |
90 |
|
91 |
Versions are compared as version nodes and the algorithm is simple: |
92 |
each component and subcomponent is compared from left to right. |
93 |
|
94 |
Anything that gives a difference decides which version is greater or lesser. |
95 |
|
96 |
Any non-existing version-node has a default value of {0}. |
97 |
|
98 |
Any non-existing element of a version node has a default value of 0. |
99 |
|
100 |
If no difference is found during the process, it would mean that both |
101 |
versions are equal. |
102 |
|
103 |
1.3.1 Concept Code |
104 |
|
105 |
#!/usr/bin/ruby |
106 |
|
107 |
class ::Array |
108 |
def adaptive_transpose |
109 |
h_size = self.max_by{ |a| a.size }.size |
110 |
v_size = self.size |
111 |
|
112 |
result = Array.new(h_size) |
113 |
with_index = self.each_with_index.to_a.freeze |
114 |
|
115 |
0.upto(h_size - 1) do |i| |
116 |
result[i] = Array.new(v_size) |
117 |
with_index.each{ |a, j| result[i][j] = a[i] } |
118 |
end |
119 |
|
120 |
result |
121 |
end |
122 |
end |
123 |
|
124 |
module Portage |
125 |
class PackageVersion |
126 |
class Node < ::Array |
127 |
def initialize(*values) |
128 |
self.concat(values) |
129 |
end |
130 |
|
131 |
def compare_with(another) |
132 |
[self, another].adaptive_transpose.each do |a, b| |
133 |
a ||= 0 |
134 |
b ||= 0 |
135 |
return -1 if a < b |
136 |
return 1 if a > b |
137 |
end |
138 |
|
139 |
return 0 |
140 |
end |
141 |
|
142 |
def self.parse(*args) |
143 |
result = new |
144 |
|
145 |
args.each do |a| |
146 |
case a |
147 |
when Integer |
148 |
result << a |
149 |
when /^[[:digit:]][[:alnum:]]*$/ |
150 |
a.scan(/[[:digit:]]+|[[:alpha:]]+/).each_with_index.map do |b, i| |
151 |
if i.even? |
152 |
str = b.gsub(/^0+/, '') |
153 |
result << (str.empty? ? 0 : Integer(str)) |
154 |
else |
155 |
value = 0 |
156 |
|
157 |
b.downcase.bytes.reverse.each_with_index do |c, i| |
158 |
value += 27 ** i * (c - 96) ## a == 1, z == 26, |
159 |
and 0 exists but is not used |
160 |
end |
161 |
|
162 |
result << value |
163 |
end |
164 |
end |
165 |
else |
166 |
raise ArgumentError.new("Invalid node string: #{a.inspect}") |
167 |
end |
168 |
end |
169 |
|
170 |
result |
171 |
end |
172 |
|
173 |
def self.zero |
174 |
@zero ||= new(0) |
175 |
end |
176 |
|
177 |
private_class_method :new, :allocate |
178 |
end |
179 |
|
180 |
attr_accessor :base, :stage, :patch, :revision |
181 |
|
182 |
def initialize(base, stage, patch, revision) |
183 |
@base, @stage, @patch, @revision = base, stage, patch, revision |
184 |
end |
185 |
|
186 |
def compare_with(another) |
187 |
[self.base, another.base].adaptive_transpose.each do |a, b| |
188 |
a ||= Node.zero |
189 |
b ||= Node.zero |
190 |
r = a.compare_with(b) |
191 |
return r unless r == 0 |
192 |
end |
193 |
|
194 |
r = self.stage.compare_with(another.stage) |
195 |
return r unless r == 0 |
196 |
|
197 |
r = self.patch.compare_with(another.patch) |
198 |
return r unless r == 0 |
199 |
|
200 |
r = self.revision.compare_with(another.revision) |
201 |
return r unless r == 0 |
202 |
|
203 |
return 0 |
204 |
end |
205 |
|
206 |
STAGES = { 'alpha' => -4, 'beta' => -3, 'pre' => -2, 'rc' => -1 } |
207 |
REGEX = /^([[:digit:]][[:alnum:]]*(?:[.][[:alnum:]]+)*)?(?:_(alpha|beta|pre|rc)([[:digit:]][[:alnum:]]*)?)?(?:_p([[:digit:]][[:alnum:]]*))?(?:-r([[:digit:]]+))?(.+)?$/m |
208 |
|
209 |
def self.parse(version_string) |
210 |
__, base, stage, stage_ver, patch, revision, extra = |
211 |
version_string.match(REGEX).to_a |
212 |
raise_invalid_version_string(version_string) if extra |
213 |
|
214 |
begin |
215 |
base = base.split('.').map{ |e| Node.parse(e) } |
216 |
stage = stage ? stage_ver ? Node.parse(STAGES[stage], |
217 |
stage_ver) : Node.parse(STAGES[stage]) : Node.zero |
218 |
patch = patch ? Node.parse(patch) : Node.zero |
219 |
revision = revision ? Node.parse(revision) : Node.zero |
220 |
rescue ArgumentError => e |
221 |
raise_invalid_version_string("#{version_string}: #{e}") |
222 |
end |
223 |
|
224 |
new(base, stage, patch, revision) |
225 |
end |
226 |
|
227 |
def self.raise_invalid_version_string(version_string) |
228 |
raise ArgumentError.new("Invalid version string: #{version_string}") |
229 |
end |
230 |
|
231 |
private_class_method :new, :allocate, :raise_invalid_version_string |
232 |
end |
233 |
end |
234 |
|
235 |
samples = [ |
236 |
["0", "0.01"], |
237 |
["0.01", "0.010"], |
238 |
["0.09", "0.090"], |
239 |
["0.10", "0.100"], |
240 |
["0.99", "0.990"], |
241 |
["0.100", "0.1000"], |
242 |
["0.100", "0.100"], |
243 |
["0.1", "0.1.1"], |
244 |
["0.1.1", "0.1a"], |
245 |
["0.1a", "0.2"], |
246 |
["0.2", "1"], |
247 |
["1", "1.0"], |
248 |
["1.0", "1.0_alpha"], |
249 |
["1.0_alpha", "1.0_alpha01"], |
250 |
["1.0_alpha01", "1.0_alpha01-r1"], |
251 |
["1.0_alpha01-r1", "1.0_alpha01_p20150105"], |
252 |
["1.0_alpha01_p20150105", "1.0_alpha01_p20150105-r1"], |
253 |
["1.0_alpha01", "1.0_beta"], |
254 |
["1.0_beta", "1.0_beta01"], |
255 |
["1.0_beta01", "1.0_pre01"], |
256 |
["1.0_pre01", "1.0_rc01"], |
257 |
["1.0_rc01", "1.0"], |
258 |
["1.0", "1.0-r1"], |
259 |
["1.0-r1", "1.0_p20150105"], |
260 |
["1.0_p20150105", "1.0_p20150105-r1"] |
261 |
] |
262 |
|
263 |
samples.each do |a, b| |
264 |
x = Portage::PackageVersion.parse(a) |
265 |
y = Portage::PackageVersion.parse(b) |
266 |
r = x.compare_with(y) |
267 |
r = r < 0 ? '<' : r > 0 ? '>' : '==' |
268 |
puts "#{a} #{r} #{b}" |
269 |
end |
270 |
|
271 |
1.3.2 Concept Code Output |
272 |
|
273 |
0 < 0.01 |
274 |
0.01 < 0.010 |
275 |
0.09 < 0.090 |
276 |
0.10 < 0.100 |
277 |
0.99 < 0.990 |
278 |
0.100 < 0.1000 |
279 |
0.100 == 0.100 |
280 |
0.1 < 0.1.1 |
281 |
0.1.1 < 0.1a |
282 |
0.1a < 0.2 |
283 |
0.2 < 1 |
284 |
1 == 1.0 |
285 |
1.0 > 1.0_alpha |
286 |
1.0_alpha < 1.0_alpha01 |
287 |
1.0_alpha01 < 1.0_alpha01-r1 |
288 |
1.0_alpha01-r1 < 1.0_alpha01_p20150105 |
289 |
1.0_alpha01_p20150105 < 1.0_alpha01_p20150105-r1 |
290 |
1.0_alpha01 < 1.0_beta |
291 |
1.0_beta < 1.0_beta01 |
292 |
1.0_beta01 < 1.0_pre01 |
293 |
1.0_pre01 < 1.0_rc01 |
294 |
1.0_rc01 < 1.0 |
295 |
1.0 < 1.0-r1 |
296 |
1.0-r1 < 1.0_p20150105 |
297 |
1.0_p20150105 < 1.0_p20150105-r1 |