@@ -141,3 +141,87 @@ export function compareLtMajor(first: string, second: string): boolean {
141141 // Compare only the major versions
142142 return firstMajor < secondMajor ;
143143}
144+
145+ export type Pre = { tag : "alpha" | "beta" | "rc" | null ; num : number } ;
146+
147+ export type ParsedVer = {
148+ major : number ;
149+ minor : number ;
150+ patch : number ;
151+ pre : Pre ; // null tag means "final"
152+ } ;
153+
154+ function parseVer ( input : string ) : ParsedVer | null {
155+ const s = input . trim ( ) . toLowerCase ( ) . replace ( / ^ v / , "" ) ;
156+ const [ core , preRaw = "" ] = s . split ( "-" , 2 ) ;
157+
158+ // allow 2 or 3 core parts; pad missing with 0
159+ const parts = core . split ( "." ) . map ( n => Number ( n ) ) ;
160+ if ( parts . length < 2 || parts . length > 3 ) {
161+ return null ;
162+ }
163+ const [ major , minor , patch = 0 ] = parts ;
164+ if ( ! [ major , minor , patch ] . every ( n => Number . isInteger ( n ) && n >= 0 ) ) {
165+ return null ;
166+ }
167+
168+ let pre : Pre = { tag : null , num : 0 } ;
169+ if ( preRaw ) {
170+ // accept alpha/beta/rc with optional number (default 0)
171+ const m = / ^ ( a l p h a | b e t a | r c ) ( \d + ) ? $ / . exec ( preRaw ) ;
172+ if ( ! m ) {
173+ return null ;
174+ }
175+ pre = { tag : m [ 1 ] as Pre [ "tag" ] , num : m [ 2 ] ? Number ( m [ 2 ] ) : 0 } ;
176+ }
177+
178+ return { major, minor, patch, pre } ;
179+ }
180+
181+ const rank : Record < NonNullable < Pre [ "tag" ] > | "final" , number > = {
182+ alpha : 0 ,
183+ beta : 1 ,
184+ rc : 2 ,
185+ final : 3 ,
186+ } ;
187+
188+ /** Compare a vs b: -1 if a<b, 0 if equal, 1 if a>b */
189+ export function compareSemverPre ( a : string , b : string ) : number {
190+ const A = parseVer ( a ) ,
191+ B = parseVer ( b ) ;
192+ if ( ! A || ! B ) {
193+ throw new Error ( `Invalid version: "${ a } " or "${ b } "` ) ;
194+ }
195+
196+ if ( A . major !== B . major ) {
197+ return A . major < B . major ? - 1 : 1 ;
198+ }
199+ if ( A . minor !== B . minor ) {
200+ return A . minor < B . minor ? - 1 : 1 ;
201+ }
202+ if ( A . patch !== B . patch ) {
203+ return A . patch < B . patch ? - 1 : 1 ;
204+ }
205+
206+ // prerelease ranking: alpha < beta < rc < final
207+ const rA = rank [ A . pre . tag ?? "final" ] ;
208+ const rB = rank [ B . pre . tag ?? "final" ] ;
209+ if ( rA !== rB ) {
210+ return rA < rB ? - 1 : 1 ;
211+ }
212+
213+ // same prerelease tag (or both final)
214+ if ( A . pre . tag === null ) {
215+ return 0 ;
216+ } // both final
217+ if ( A . pre . num !== B . pre . num ) {
218+ return A . pre . num < B . pre . num ? - 1 : 1 ;
219+ }
220+
221+ return 0 ;
222+ }
223+
224+ export const geSemverPre = ( a : string , b : string ) : boolean =>
225+ compareSemverPre ( a , b ) >= 0 ;
226+ export const ltSemverPre = ( a : string , b : string ) : boolean =>
227+ compareSemverPre ( a , b ) < 0 ;
0 commit comments