feat: TestRNG 클래스 추가 및 관련 테스트 케이스 작성
This commit is contained in:
@@ -8,3 +8,4 @@ export * from './util/convertBytesLikeToUint8Array.js';
|
||||
export * from './util/LiteHashDRBG.js';
|
||||
export * from './util/RNG.js';
|
||||
export * from './util/RandUtil.js';
|
||||
export * from './util/TestRNG.js';
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import type { RNG } from './RNG.js';
|
||||
|
||||
const maxSafeInt = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const clamp01 = (value: number): number => {
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value > 1) {
|
||||
return 1;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 테스트에서 0/1만 고정으로 뽑기 위한 RNG
|
||||
export class ConstantRNG implements RNG {
|
||||
private readonly bit: 0 | 1;
|
||||
|
||||
public constructor(bit: 0 | 1) {
|
||||
this.bit = bit;
|
||||
}
|
||||
|
||||
public getMaxInt(): number {
|
||||
return maxSafeInt;
|
||||
}
|
||||
|
||||
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||
if (bytes <= 0) {
|
||||
throw new Error('bytes must be positive');
|
||||
}
|
||||
const result = new Uint8Array(bytes);
|
||||
result.fill(this.bit === 0 ? 0x00 : 0xff);
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||
if (bits <= 0) {
|
||||
throw new Error('bits must be positive');
|
||||
}
|
||||
const bytes = (bits + 7) >> 3;
|
||||
const headBits = bits & 0x7;
|
||||
const result = this.nextBytes(bytes);
|
||||
|
||||
if (headBits === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result[bytes - 1]! &= 0xff >> (8 - headBits);
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextInt(max?: number): number {
|
||||
if (max === undefined || max === maxSafeInt) {
|
||||
return this.bit === 0 ? 0 : maxSafeInt;
|
||||
}
|
||||
if (max > maxSafeInt) {
|
||||
throw new Error('Over max int');
|
||||
}
|
||||
if (max === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (max < 0) {
|
||||
return -this.nextInt(-max);
|
||||
}
|
||||
return this.bit === 0 ? 0 : max;
|
||||
}
|
||||
|
||||
public nextFloat1(): number {
|
||||
return this.bit;
|
||||
}
|
||||
}
|
||||
|
||||
// 중간값 고정 + bool은 0/1 교대로 뽑는 RNG
|
||||
export class MidpointRNG implements RNG {
|
||||
private bitState: 0 | 1;
|
||||
|
||||
public constructor(startBit: 0 | 1 = 0) {
|
||||
this.bitState = startBit;
|
||||
}
|
||||
|
||||
public getMaxInt(): number {
|
||||
return maxSafeInt;
|
||||
}
|
||||
|
||||
private nextBitRaw(): 0 | 1 {
|
||||
const value = this.bitState;
|
||||
this.bitState = value === 0 ? 1 : 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||
if (bytes <= 0) {
|
||||
throw new Error('bytes must be positive');
|
||||
}
|
||||
return this.nextBits(bytes * 8);
|
||||
}
|
||||
|
||||
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||
if (bits <= 0) {
|
||||
throw new Error('bits must be positive');
|
||||
}
|
||||
const bytes = (bits + 7) >> 3;
|
||||
const result = new Uint8Array(bytes);
|
||||
|
||||
for (let bitIdx = 0; bitIdx < bits; bitIdx += 1) {
|
||||
if (this.nextBitRaw() === 0) {
|
||||
continue;
|
||||
}
|
||||
const byteIdx = bitIdx >> 3;
|
||||
const offset = bitIdx & 0x7;
|
||||
result[byteIdx]! |= 1 << offset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextInt(max?: number): number {
|
||||
if (max === undefined || max === maxSafeInt) {
|
||||
return Math.floor(maxSafeInt / 2);
|
||||
}
|
||||
if (max > maxSafeInt) {
|
||||
throw new Error('Over max int');
|
||||
}
|
||||
if (max === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (max < 0) {
|
||||
return -this.nextInt(-max);
|
||||
}
|
||||
return Math.floor(max / 2);
|
||||
}
|
||||
|
||||
public nextFloat1(): number {
|
||||
return 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 사인파 기반으로 주기/진폭을 조절하는 RNG
|
||||
export class SineRNG implements RNG {
|
||||
private step = 0;
|
||||
private readonly period: number;
|
||||
private readonly amplitude: number;
|
||||
private readonly phase: number;
|
||||
|
||||
public constructor(period = 32, amplitude = 0.5, phase = 0) {
|
||||
if (period <= 0) {
|
||||
throw new Error('period must be positive');
|
||||
}
|
||||
this.period = period;
|
||||
this.amplitude = amplitude;
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public getMaxInt(): number {
|
||||
return maxSafeInt;
|
||||
}
|
||||
|
||||
private nextWaveFloat(): number {
|
||||
const radians = this.phase + (this.step * 2 * Math.PI) / this.period;
|
||||
const value = 0.5 + this.amplitude * Math.sin(radians);
|
||||
this.step += 1;
|
||||
return clamp01(value);
|
||||
}
|
||||
|
||||
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||
if (bytes <= 0) {
|
||||
throw new Error('bytes must be positive');
|
||||
}
|
||||
const result = new Uint8Array(bytes);
|
||||
for (let idx = 0; idx < bytes; idx += 1) {
|
||||
const value = Math.floor(this.nextWaveFloat() * 256);
|
||||
result[idx] = value >= 256 ? 255 : value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||
if (bits <= 0) {
|
||||
throw new Error('bits must be positive');
|
||||
}
|
||||
const bytes = (bits + 7) >> 3;
|
||||
const headBits = bits & 0x7;
|
||||
const result = this.nextBytes(bytes);
|
||||
|
||||
if (headBits === 0) {
|
||||
return result;
|
||||
}
|
||||
result[bytes - 1]! &= 0xff >> (8 - headBits);
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextInt(max?: number): number {
|
||||
if (max === undefined || max === maxSafeInt) {
|
||||
const value = Math.floor(this.nextWaveFloat() * (maxSafeInt + 1));
|
||||
return value > maxSafeInt ? maxSafeInt : value;
|
||||
}
|
||||
if (max > maxSafeInt) {
|
||||
throw new Error('Over max int');
|
||||
}
|
||||
if (max === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (max < 0) {
|
||||
return -this.nextInt(-max);
|
||||
}
|
||||
const value = Math.floor(this.nextWaveFloat() * (max + 1));
|
||||
return value > max ? max : value;
|
||||
}
|
||||
|
||||
public nextFloat1(): number {
|
||||
return this.nextWaveFloat();
|
||||
}
|
||||
}
|
||||
|
||||
// 지정한 수열을 반복 재생하는 테스트용 RNG
|
||||
export class SequenceRNG implements RNG {
|
||||
private readonly sequence: number[];
|
||||
private idx = 0;
|
||||
|
||||
public constructor(sequence: number[]) {
|
||||
if (sequence.length === 0) {
|
||||
throw new Error('sequence must not be empty');
|
||||
}
|
||||
this.sequence = sequence.map(clamp01);
|
||||
}
|
||||
|
||||
public getMaxInt(): number {
|
||||
return maxSafeInt;
|
||||
}
|
||||
|
||||
private nextValue(): number {
|
||||
const value = this.sequence[this.idx]!;
|
||||
this.idx = (this.idx + 1) % this.sequence.length;
|
||||
return value;
|
||||
}
|
||||
|
||||
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||
if (bytes <= 0) {
|
||||
throw new Error('bytes must be positive');
|
||||
}
|
||||
const result = new Uint8Array(bytes);
|
||||
for (let idx = 0; idx < bytes; idx += 1) {
|
||||
const value = Math.floor(this.nextValue() * 256);
|
||||
result[idx] = value >= 256 ? 255 : value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||
if (bits <= 0) {
|
||||
throw new Error('bits must be positive');
|
||||
}
|
||||
const bytes = (bits + 7) >> 3;
|
||||
const headBits = bits & 0x7;
|
||||
const result = this.nextBytes(bytes);
|
||||
if (headBits === 0) {
|
||||
return result;
|
||||
}
|
||||
result[bytes - 1]! &= 0xff >> (8 - headBits);
|
||||
return result;
|
||||
}
|
||||
|
||||
public nextInt(max?: number): number {
|
||||
if (max === undefined || max === maxSafeInt) {
|
||||
const value = Math.floor(this.nextValue() * (maxSafeInt + 1));
|
||||
return value > maxSafeInt ? maxSafeInt : value;
|
||||
}
|
||||
if (max > maxSafeInt) {
|
||||
throw new Error('Over max int');
|
||||
}
|
||||
if (max === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (max < 0) {
|
||||
return -this.nextInt(-max);
|
||||
}
|
||||
const value = Math.floor(this.nextValue() * (max + 1));
|
||||
return value > max ? max : value;
|
||||
}
|
||||
|
||||
public nextFloat1(): number {
|
||||
return this.nextValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ConstantRNG, MidpointRNG, SequenceRNG, SineRNG } from '../src/util/TestRNG.js';
|
||||
|
||||
const toArray = (bytes: Uint8Array): number[] => Array.from(bytes);
|
||||
|
||||
describe('TestRNG:Constant', () => {
|
||||
it('returns fixed 0', () => {
|
||||
const rng = new ConstantRNG(0);
|
||||
expect(rng.nextFloat1()).toBe(0);
|
||||
expect(rng.nextInt(10)).toBe(0);
|
||||
expect(toArray(rng.nextBytes(3))).toEqual([0, 0, 0]);
|
||||
expect(toArray(rng.nextBits(3))).toEqual([0]);
|
||||
});
|
||||
|
||||
it('returns fixed 1', () => {
|
||||
const rng = new ConstantRNG(1);
|
||||
expect(rng.nextFloat1()).toBe(1);
|
||||
expect(rng.nextInt(10)).toBe(10);
|
||||
expect(toArray(rng.nextBytes(2))).toEqual([255, 255]);
|
||||
expect(toArray(rng.nextBits(3))).toEqual([7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestRNG:Midpoint', () => {
|
||||
it('returns midpoint for int/float', () => {
|
||||
const rng = new MidpointRNG();
|
||||
expect(rng.nextFloat1()).toBe(0.5);
|
||||
expect(rng.nextInt(9)).toBe(4);
|
||||
expect(rng.nextInt(10)).toBe(5);
|
||||
});
|
||||
|
||||
it('alternates bits', () => {
|
||||
const rng = new MidpointRNG();
|
||||
expect(toArray(rng.nextBits(4))).toEqual([10]);
|
||||
expect(toArray(rng.nextBits(4))).toEqual([10]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestRNG:Sine', () => {
|
||||
it('follows sine wave with period/amplitude', () => {
|
||||
const rng = new SineRNG(4, 0.5, 0);
|
||||
expect(rng.nextFloat1()).toBeCloseTo(0.5, 8);
|
||||
expect(rng.nextFloat1()).toBeCloseTo(1, 8);
|
||||
expect(rng.nextFloat1()).toBeCloseTo(0.5, 8);
|
||||
expect(rng.nextFloat1()).toBeCloseTo(0, 8);
|
||||
});
|
||||
|
||||
it('maps float to int range', () => {
|
||||
const rng = new SineRNG(4, 0.5, 0);
|
||||
expect(rng.nextInt(9)).toBe(5);
|
||||
expect(rng.nextInt(9)).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestRNG:Sequence', () => {
|
||||
it('cycles fixed sequence', () => {
|
||||
const rng = new SequenceRNG([0, 0.25, 0.5, 0.75, 1]);
|
||||
expect(rng.nextFloat1()).toBe(0);
|
||||
expect(rng.nextFloat1()).toBe(0.25);
|
||||
expect(rng.nextFloat1()).toBe(0.5);
|
||||
expect(rng.nextFloat1()).toBe(0.75);
|
||||
expect(rng.nextFloat1()).toBe(1);
|
||||
expect(rng.nextFloat1()).toBe(0);
|
||||
});
|
||||
|
||||
it('converts sequence to bytes and ints', () => {
|
||||
const rng = new SequenceRNG([0, 0.5, 1]);
|
||||
expect(toArray(rng.nextBytes(3))).toEqual([0, 128, 255]);
|
||||
expect(rng.nextInt(8)).toBe(0);
|
||||
expect(rng.nextInt(8)).toBe(4);
|
||||
expect(rng.nextInt(8)).toBe(8);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user