@@ -90,6 +90,9 @@ type NationalIdenityNumberOptions = ValidatorOptions & {
9090 format ?: NationalIdentityNumberFormat ;
9191} ;
9292
93+ // the first two digts are optional, as they're the century in the long format version
94+ const PERSONNUMMER_FORMAT = / ^ ( \d { 2 } ) { 0 , 1 } ( \d { 2 } ) ( \d { 2 } ) ( \d { 2 } ) ( [ + - ] ? ) ( \d { 4 } ) $ / ;
95+
9396/**
9497 * Validates that the input value is a Swedish national identity number (personnummer or samordningsnummer).
9598 *
@@ -108,40 +111,55 @@ export function validateNationalIdentityNumber(
108111 value : string ,
109112 options : NationalIdenityNumberOptions = { } ,
110113) : boolean {
111- if ( options . allowFormatting ) {
112- // biome-ignore lint/style/noParameterAssign:
113- value = stripFormatting ( value ) ;
114+ const match = PERSONNUMMER_FORMAT . exec ( value ) ;
115+
116+ if ( ! match ) {
117+ return false ;
114118 }
115119
116- const isLongFormat = value . length === 12 ;
120+ const [ _ , century , year , month , day , separator , rest ] = match ;
117121
118- // when verifying the value, we must always use the short format.
119- // because the long format would generate a different checksum
120- const isValid = mod10 ( isLongFormat ? value . substring ( 2 ) : value ) ;
121- if ( ! isValid ) {
122+ if ( century && options . format === 'short' ) {
122123 return false ;
123124 }
124125
125- // this allows us to handle both YYYYMMDD and YYMMDD when extracting the date
126- const offset = isLongFormat ? 2 : 0 ;
127- // copy/inspiration from NAV https://github.com/navikt/fnrvalidator/blob/77e57f0bc8e3570ddc2f0a94558c58d0f7259fe0/src/validator.ts#L108
128- let year = Number ( value . substring ( 0 , 2 + offset ) ) ;
129- const month = Number ( value . substring ( 2 + offset , 4 + offset ) ) ;
130- let day = Number ( value . substring ( 4 + offset , 6 + offset ) ) ;
126+ if ( separator && ! options . allowFormatting ) {
127+ return false ;
128+ }
131129
132- // 1900 isn't a leap year, but 2000 is. Since JS two digits years to the Date constructor is an offset from the year 1900
133- // we need to special handle that case. For other cases it doesn't really matter if the year is 1925 or 2025.
134- if ( ! isLongFormat && year === 0 ) {
135- year = 2000 ;
130+ // when verifying the value, we must always use the short format, discaring the century
131+ // if included it would generate a different checksum
132+ const isValid = mod10 ( `${ year } ${ month } ${ day } ${ rest } ` ) ;
133+ if ( ! isValid ) {
134+ return false ;
136135 }
137136
137+ // // this allows us to handle both YYYYMMDD and YYMMDD when extracting the date
138+ // const offset = isLongFormat ? 2 : 0;
139+ // // copy/inspiration from NAV https://github.com/navikt/fnrvalidator/blob/77e57f0bc8e3570ddc2f0a94558c58d0f7259fe0/src/validator.ts#L108
140+ // let year = Number(value.substring(0, 2 + offset));
141+ // const month = Number(value.substring(2 + offset, 4 + offset));
142+ // let day = Number(value.substring(4 + offset, 6 + offset));
143+ //
144+
145+ const month2 = Number ( month ) ;
146+ let day2 = Number ( day ) ;
147+ let year2 = Number ( century ? century + year : year ) ;
138148 // for a samordningsnummer the day is increased by 60. Eg the 31st of a month would be 91, or the 3rd would be 63.
139149 // thus we need to subtract 60 to get the correct day of the month
140- if ( day > 60 ) {
141- day = day - 60 ;
150+ if ( day2 > 60 ) {
151+ day2 = day2 - 60 ;
152+ }
153+
154+ // 1900 isn't a leap year, but 2000 is. Since JS two digits years to the Date constructor is an offset from the year 1900
155+ // we need to special handle that case. For other cases it doesn't really matter if the year is 1925 or 2025.
156+ if ( ! separator && ! century && year === '00' ) {
157+ year2 = 2000 ;
158+ } else if ( century ) {
159+ year2 = Number ( century + year ) ;
142160 }
143161
144- return isValidDate ( year , month , day ) ;
162+ return isValidDate ( year2 , month2 , day2 ) ;
145163}
146164
147165// just reexport the no method for API feature parity
0 commit comments