각각 [-0], [-NaN]가 주어졌을 때, 예상과 다르게 포매팅이 진행된다는 것이다.
✓ test/basic.test.ts (2)
✓ -0 // ⛳
✓ -NaN // ⛳
✓ test/basic.test.ts (2)
✓ 0 // ⛳
✓ NaN // ⛳
다음은 vitest에 추상화되어있는 test suite의 description(describe, test, it)의 문자열 포매팅 함수(format)의 일부이다.
case '%f': return Number.parseFloat(String(args[i++])).toString()
String(-0)
// '0'
String(-NaN)
// 'NaN'
분기처리를 통해 edgeCase를 처리하였다.
이 과정에서 -0
을 처리하는 것은 문제가 없었지만 -NaN
을 처리하는 것은 조금 난이도가 있었다.
이유는 아래와 같다.
NaN;
// > NaN
-NaN;
// > NaN // ⛳ -NaN은 초기화 시점에서 -NaN이 아니라 NaN으로 평가된다.
NaN === NaN;
// false
Object.is(NaN, -NaN); // ⛳
// true
JS가 -NaN을 추상화 하는 과정에서 위 사이드 이펙트를 고려하지 못한 것 같았다.
따라서, 정상적인 negative 값인지 판단하는 것이 불가능하다.
String(-NaN);
// 'NaN'
검색하며 문제 해결에 대한 아이디어를 찾아보았다.
IEEE와 관련된 웹사이트를 돌아다니다 NaN에 대한 규약에서 아이디어를 얻었고, JS에서 sign bit에 접근하여 이에 대한 값(부호)를 확인할 수 있다면 해결할 수 있는 문제였다.
값을 64비트 부동 소수점 배열로 변경한 뒤, 값 할당 이후 buffer값을 32비트 정수 배열로 바꿔 signIndex(최상위 인덱스. 즉, 부호)에 접근하여 1(음수)인지 확인하면 -NaN의 부호를 확인할 수 있는지 확인할 수 있었다.
/**
* Checks if a given number is NaN and if it is a negative NaN.
*
* @param {number} val - The number to check.
* @returns {boolean} - True if the number is a negative NaN, false otherwise.
*/
export function isNegativeNaN(val: number): boolean {
// NaN이 아니면 ealry return.
if (!Number.isNaN(val)) return false;
// 64비트배열 생성
const f64 = new Float64Array(1);
// Float64Array [0, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 1, Symbol(Symbol.toStringTag): 'Float64Array']
// 값 할당
f64[0] = val;
// Float64Array [NaN, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 1, Symbol(Symbol.toStringTag): 'Float64Array']
// buffer값으로 32비트 정수 배열 생성
const u32 = new Uint32Array(f64.buffer);
// Uint32Array(2) [0, 4294443008, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 2, Symbol(Symbol.toStringTag): 'Uint32Array']
// 상위 32비트(index 1)의 마지막 비트가 음수(1)인지 확인
const isNegative = u32[1] >>> 31 === 1;
return isNegative;
}
잘 작동한다. 👍
isNegativeNaN(-NaN);
// true
isNegativeNaN(NaN);
// false
isNegativeNaN(-0);
// false
// ...
애초에 -NaN을 쓴다는 것 자체가 잘못된 개념이긴 하지만, 이에 관한 엣지케이스가 대부분의 라이브러리에 되어있지 않다는 것을 확인하였다.
조만간 vitest 말고도 다른 오픈소스에 작업을 진행할 예정이다.
PR 후, 다른사람들 편하게 쓰라고 배포까지 완료하였다.
sheremet 형님 : 거기 바꾸면 breakChange 되니까 다른 방식으로 해결하세요. 저도 고민중임.
잠시 슬픔과 죄송함의 복합적인 감정을 음미하였다.
뭔가 내 부족한 지식으로 메인테이너 형님의 시간을 빼았아, 폐가 되었기 때문이었을까.
이번 피드백으로 또 새로운 것을 배웠지만, 그래도 죄송한 마음은 어쩔 수 없을 따름이다.
format 자체에서 판단하는 것이 아닌, runner의 formatTitle에서 정규표현식으로 %f를 추출하는 과정에서 해당 값이 -0이나 -NaN일 경우 %f를 -%f로 바꾸는 것. 이 방식이 모든 코드를 살펴보았을 때, 셰르멧 형님이 원하는대로 변경점은 최소화가 된다.
금요일 퇴근 후, 시간이 조금 있어 자기 전 PR을 완성하였다.
그렇기에 회귀 테스트를 작성할 수 없어서, 방향성에 대한 피드백을 구했지만 스크린샷으로 올렸던 자체 테스트 + sheremet 형님이 직접 테스트로 확인하셨는지 그냥 merge 때리셨다.