diff options
| author | David Pollack <[email protected]> | 2025-06-16 00:52:49 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-16 00:52:49 +0200 |
| commit | d07d96d01833085f2d3c5f9c851a572ebf8c47df (patch) | |
| tree | b72fa0a7d325744a3c616baf742c8837583b6739 | |
| parent | f08b24374c81c11adff2a68015483a41ae06d9d3 (diff) | |
| download | faker-d07d96d01833085f2d3c5f9c851a572ebf8c47df.tar.xz faker-d07d96d01833085f2d3c5f9c851a572ebf8c47df.zip | |
feat(location): simple coordinate methods (#3528)
| -rw-r--r-- | src/index.ts | 2 | ||||
| -rw-r--r-- | src/modules/location/index.ts | 348 | ||||
| -rw-r--r-- | src/simple-faker.ts | 3 | ||||
| -rw-r--r-- | test/modules/location.spec.ts | 106 |
4 files changed, 238 insertions, 221 deletions
diff --git a/src/index.ts b/src/index.ts index a8c4ecea..c100642a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -69,7 +69,7 @@ export type { HelpersModule, SimpleHelpersModule } from './modules/helpers'; export type { ImageModule } from './modules/image'; export { IPv4Network } from './modules/internet'; export type { IPv4NetworkType, InternetModule } from './modules/internet'; -export type { LocationModule } from './modules/location'; +export type { LocationModule, SimpleLocationModule } from './modules/location'; export type { LoremModule } from './modules/lorem'; export type { MusicModule } from './modules/music'; export type { NumberModule } from './modules/number'; diff --git a/src/modules/location/index.ts b/src/modules/location/index.ts index 29267eb1..40d065b0 100644 --- a/src/modules/location/index.ts +++ b/src/modules/location/index.ts @@ -1,5 +1,6 @@ +import type { Faker } from '../..'; import { FakerError } from '../../errors/faker-error'; -import { ModuleBase } from '../../internal/module-base'; +import { SimpleModuleBase } from '../../internal/module-base'; /** * Represents a language with its full name, 2 character ISO 639-1 code, and 3 character ISO 639-2 code. @@ -22,6 +23,178 @@ export interface Language { } /** + * Module with location functions that don't require localized data + */ +export class SimpleLocationModule extends SimpleModuleBase { + /** + * Generates a random latitude. + * + * @param options An options object. + * @param options.max The upper bound for the latitude to generate. Defaults to `90`. + * @param options.min The lower bound for the latitude to generate. Defaults to `-90`. + * @param options.precision The number of decimal points of precision for the latitude. Defaults to `4`. + * + * @example + * faker.location.latitude() // -30.9501 + * faker.location.latitude({ max: 10 }) // 5.7225 + * faker.location.latitude({ max: 10, min: -10 }) // -9.6273 + * faker.location.latitude({ max: 10, min: -10, precision: 5 }) // 2.68452 + * + * @since 8.0.0 + */ + latitude( + options: { + /** + * The upper bound for the latitude to generate. + * + * @default 90 + */ + max?: number; + /** + * The lower bound for the latitude to generate. + * + * @default -90 + */ + min?: number; + /** + * The number of decimal points of precision for the latitude. + * + * @default 4 + */ + precision?: number; + } = {} + ): number { + const { max = 90, min = -90, precision = 4 } = options; + + return this.faker.number.float({ min, max, fractionDigits: precision }); + } + + /** + * Generates a random longitude. + * + * @param options An options object. + * @param options.max The upper bound for the longitude to generate. Defaults to `180`. + * @param options.min The lower bound for the longitude to generate. Defaults to `-180`. + * @param options.precision The number of decimal points of precision for the longitude. Defaults to `4`. + * + * @example + * faker.location.longitude() // -30.9501 + * faker.location.longitude({ max: 10 }) // 5.7225 + * faker.location.longitude({ max: 10, min: -10 }) // -9.6273 + * faker.location.longitude({ max: 10, min: -10, precision: 5 }) // 2.68452 + * + * @since 8.0.0 + */ + longitude( + options: { + /** + * The upper bound for the longitude to generate. + * + * @default 180 + */ + max?: number; + /** + * The lower bound for the longitude to generate. + * + * @default -180 + */ + min?: number; + /** + * The number of decimal points of precision for the longitude. + * + * @default 4 + */ + precision?: number; + } = {} + ): number { + const { max = 180, min = -180, precision = 4 } = options; + + return this.faker.number.float({ max, min, fractionDigits: precision }); + } + + /** + * Generates a random GPS coordinate within the specified radius from the given coordinate. + * + * @param options The options for generating a GPS coordinate. + * @param options.origin The original coordinate to get a new coordinate close to. + * If no coordinate is given, a random one will be chosen. + * @param options.radius The maximum distance from the given coordinate to the new coordinate. Defaults to `10`. + * @param options.isMetric If `true` assume the radius to be in kilometers. If `false` for miles. Defaults to `false`. + * + * @example + * faker.location.nearbyGPSCoordinate() // [ 33.8475, -170.5953 ] + * faker.location.nearbyGPSCoordinate({ origin: [33, -170] }) // [ 33.0165, -170.0636 ] + * faker.location.nearbyGPSCoordinate({ origin: [33, -170], radius: 1000, isMetric: true }) // [ 37.9163, -179.2408 ] + * + * @since 8.0.0 + */ + nearbyGPSCoordinate( + options: { + /** + * The original coordinate to get a new coordinate close to. + */ + origin?: [latitude: number, longitude: number]; + /** + * The maximum distance from the given coordinate to the new coordinate. + * + * @default 10 + */ + radius?: number; + /** + * If `true` assume the radius to be in kilometers. If `false` for miles. + * + * @default false + */ + isMetric?: boolean; + } = {} + ): [latitude: number, longitude: number] { + const { origin, radius = 10, isMetric = false } = options; + + // If there is no origin, the best we can do is return a random GPS coordinate. + if (origin == null) { + return [this.latitude(), this.longitude()]; + } + + const angleRadians = this.faker.number.float({ + max: 2 * Math.PI, + fractionDigits: 5, + }); // in ° radians + + const radiusMetric = isMetric ? radius : radius * 1.60934; // in km + const errorCorrection = 0.995; // avoid float issues + const distanceInKm = + this.faker.number.float({ + max: radiusMetric, + fractionDigits: 3, + }) * errorCorrection; // in km + + /** + * The distance in km per degree for earth. + */ + const kmPerDegree = 40_000 / 360; // in km/° + + const distanceInDegree = distanceInKm / kmPerDegree; // in ° + + const coordinate: [latitude: number, longitude: number] = [ + origin[0] + Math.sin(angleRadians) * distanceInDegree, + origin[1] + Math.cos(angleRadians) * distanceInDegree, + ]; + + // Box latitude [-90°, 90°] + coordinate[0] = coordinate[0] % 180; + if (coordinate[0] < -90 || coordinate[0] > 90) { + coordinate[0] = Math.sign(coordinate[0]) * 180 - coordinate[0]; + coordinate[1] += 180; + } + + // Box longitude [-180°, 180°] + coordinate[1] = (((coordinate[1] % 360) + 540) % 360) - 180; + + return [coordinate[0], coordinate[1]]; + } +} + +/** * Module to generate addresses and locations. Prior to Faker 8.0.0, this module was known as `faker.address`. * * ### Overview @@ -32,7 +205,11 @@ export interface Language { * * For a random country, you can use [`country()`](https://fakerjs.dev/api/location.html#country) or [`countryCode()`](https://fakerjs.dev/api/location.html#countrycode). */ -export class LocationModule extends ModuleBase { +export class LocationModule extends SimpleLocationModule { + constructor(protected readonly faker: Faker) { + super(faker); + } + /** * Generates random zip code from specified format. If format is not specified, * the locale's zip format is used. @@ -351,92 +528,6 @@ export class LocationModule extends ModuleBase { } /** - * Generates a random latitude. - * - * @param options An options object. - * @param options.max The upper bound for the latitude to generate. Defaults to `90`. - * @param options.min The lower bound for the latitude to generate. Defaults to `-90`. - * @param options.precision The number of decimal points of precision for the latitude. Defaults to `4`. - * - * @example - * faker.location.latitude() // -30.9501 - * faker.location.latitude({ max: 10 }) // 5.7225 - * faker.location.latitude({ max: 10, min: -10 }) // -9.6273 - * faker.location.latitude({ max: 10, min: -10, precision: 5 }) // 2.68452 - * - * @since 8.0.0 - */ - latitude( - options: { - /** - * The upper bound for the latitude to generate. - * - * @default 90 - */ - max?: number; - /** - * The lower bound for the latitude to generate. - * - * @default -90 - */ - min?: number; - /** - * The number of decimal points of precision for the latitude. - * - * @default 4 - */ - precision?: number; - } = {} - ): number { - const { max = 90, min = -90, precision = 4 } = options; - - return this.faker.number.float({ min, max, fractionDigits: precision }); - } - - /** - * Generates a random longitude. - * - * @param options An options object. - * @param options.max The upper bound for the longitude to generate. Defaults to `180`. - * @param options.min The lower bound for the longitude to generate. Defaults to `-180`. - * @param options.precision The number of decimal points of precision for the longitude. Defaults to `4`. - * - * @example - * faker.location.longitude() // -30.9501 - * faker.location.longitude({ max: 10 }) // 5.7225 - * faker.location.longitude({ max: 10, min: -10 }) // -9.6273 - * faker.location.longitude({ max: 10, min: -10, precision: 5 }) // 2.68452 - * - * @since 8.0.0 - */ - longitude( - options: { - /** - * The upper bound for the longitude to generate. - * - * @default 180 - */ - max?: number; - /** - * The lower bound for the longitude to generate. - * - * @default -180 - */ - min?: number; - /** - * The number of decimal points of precision for the longitude. - * - * @default 4 - */ - precision?: number; - } = {} - ): number { - const { max = 180, min = -180, precision = 4 } = options; - - return this.faker.number.float({ max, min, fractionDigits: precision }); - } - - /** * Returns a random direction (cardinal and ordinal; northwest, east, etc). * * @param options The options to use. @@ -550,87 +641,6 @@ export class LocationModule extends ModuleBase { } /** - * Generates a random GPS coordinate within the specified radius from the given coordinate. - * - * @param options The options for generating a GPS coordinate. - * @param options.origin The original coordinate to get a new coordinate close to. - * If no coordinate is given, a random one will be chosen. - * @param options.radius The maximum distance from the given coordinate to the new coordinate. Defaults to `10`. - * @param options.isMetric If `true` assume the radius to be in kilometers. If `false` for miles. Defaults to `false`. - * - * @example - * faker.location.nearbyGPSCoordinate() // [ 33.8475, -170.5953 ] - * faker.location.nearbyGPSCoordinate({ origin: [33, -170] }) // [ 33.0165, -170.0636 ] - * faker.location.nearbyGPSCoordinate({ origin: [33, -170], radius: 1000, isMetric: true }) // [ 37.9163, -179.2408 ] - * - * @since 8.0.0 - */ - nearbyGPSCoordinate( - options: { - /** - * The original coordinate to get a new coordinate close to. - */ - origin?: [latitude: number, longitude: number]; - /** - * The maximum distance from the given coordinate to the new coordinate. - * - * @default 10 - */ - radius?: number; - /** - * If `true` assume the radius to be in kilometers. If `false` for miles. - * - * @default false - */ - isMetric?: boolean; - } = {} - ): [latitude: number, longitude: number] { - const { origin, radius = 10, isMetric = false } = options; - - // If there is no origin, the best we can do is return a random GPS coordinate. - if (origin == null) { - return [this.latitude(), this.longitude()]; - } - - const angleRadians = this.faker.number.float({ - max: 2 * Math.PI, - fractionDigits: 5, - }); // in ° radians - - const radiusMetric = isMetric ? radius : radius * 1.60934; // in km - const errorCorrection = 0.995; // avoid float issues - const distanceInKm = - this.faker.number.float({ - max: radiusMetric, - fractionDigits: 3, - }) * errorCorrection; // in km - - /** - * The distance in km per degree for earth. - */ - const kmPerDegree = 40_000 / 360; // in km/° - - const distanceInDegree = distanceInKm / kmPerDegree; // in ° - - const coordinate: [latitude: number, longitude: number] = [ - origin[0] + Math.sin(angleRadians) * distanceInDegree, - origin[1] + Math.cos(angleRadians) * distanceInDegree, - ]; - - // Box latitude [-90°, 90°] - coordinate[0] = coordinate[0] % 180; - if (coordinate[0] < -90 || coordinate[0] > 90) { - coordinate[0] = Math.sign(coordinate[0]) * 180 - coordinate[0]; - coordinate[1] += 180; - } - - // Box longitude [-180°, 180°] - coordinate[1] = (((coordinate[1] % 360) + 540) % 360) - 180; - - return [coordinate[0], coordinate[1]]; - } - - /** * Returns a random IANA time zone relevant to this locale. * * The returned time zone is tied to the current locale. diff --git a/src/simple-faker.ts b/src/simple-faker.ts index 35fbc866..7f86a389 100644 --- a/src/simple-faker.ts +++ b/src/simple-faker.ts @@ -2,6 +2,7 @@ import { randomSeed } from './internal/seed'; import { DatatypeModule } from './modules/datatype'; import { SimpleDateModule } from './modules/date'; import { SimpleHelpersModule } from './modules/helpers'; +import { SimpleLocationModule } from './modules/location'; import { NumberModule } from './modules/number'; import { StringModule } from './modules/string'; import type { Randomizer } from './randomizer'; @@ -14,6 +15,7 @@ import { generateMersenne53Randomizer } from './utils/mersenne'; * - `datatype` * - `date` (without `month` and `weekday`) * - `helpers` (without `fake`) + * - `location` (`latitude`, `longitude` and `nearbyGPSCoordinate` only) * - `number` * - `string` * @@ -85,6 +87,7 @@ export class SimpleFaker { readonly datatype: DatatypeModule = new DatatypeModule(this); readonly date: SimpleDateModule = new SimpleDateModule(this); readonly helpers: SimpleHelpersModule = new SimpleHelpersModule(this); + readonly location: SimpleLocationModule = new SimpleLocationModule(this); readonly number: NumberModule = new NumberModule(this); readonly string: StringModule = new StringModule(this); diff --git a/test/modules/location.spec.ts b/test/modules/location.spec.ts index 45d789a4..7ca81672 100644 --- a/test/modules/location.spec.ts +++ b/test/modules/location.spec.ts @@ -8,6 +8,7 @@ import { faker, fakerEN_CA, fakerEN_US, + simpleFaker, } from '../../src'; import { seededTests } from '../support/seeded-runs'; import { times } from './../support/times'; @@ -247,22 +248,22 @@ describe('location', () => { }); }); - describe('latitude()', () => { + describe.each([faker, simpleFaker])('latitude()', (fakerFn) => { it('returns a number', () => { - const latitude = faker.location.latitude(); + const latitude = fakerFn.location.latitude(); expect(latitude).toBeTypeOf('number'); }); it('returns random latitude', () => { - const latitude = faker.location.latitude(); + const latitude = fakerFn.location.latitude(); expect(latitude).toBeGreaterThanOrEqual(-90.0); expect(latitude).toBeLessThanOrEqual(90.0); }); it('returns latitude with min and max and default precision', () => { - const latitude = faker.location.latitude({ max: 5, min: -5 }); + const latitude = fakerFn.location.latitude({ max: 5, min: -5 }); expect( precision(latitude), @@ -274,7 +275,7 @@ describe('location', () => { }); it('returns random latitude with custom precision', () => { - const latitude = faker.location.latitude({ precision: 7 }); + const latitude = fakerFn.location.latitude({ precision: 7 }); expect( precision(latitude), @@ -286,22 +287,22 @@ describe('location', () => { }); }); - describe('longitude()', () => { + describe.each([faker, simpleFaker])('longitude()', (fakerFn) => { it('returns a number', () => { - const longitude = faker.location.longitude(); + const longitude = fakerFn.location.longitude(); expect(longitude).toBeTypeOf('number'); }); it('returns random longitude', () => { - const longitude = faker.location.longitude(); + const longitude = fakerFn.location.longitude(); expect(longitude).toBeGreaterThanOrEqual(-180); expect(longitude).toBeLessThanOrEqual(180); }); it('returns random longitude with min and max and default precision', () => { - const longitude = faker.location.longitude({ max: 100, min: -30 }); + const longitude = fakerFn.location.longitude({ max: 100, min: -30 }); expect( precision(longitude), @@ -313,7 +314,7 @@ describe('location', () => { }); it('returns random longitude with custom precision', () => { - const longitude = faker.location.longitude({ precision: 7 }); + const longitude = fakerFn.location.longitude({ precision: 7 }); expect( precision(longitude), @@ -376,47 +377,50 @@ describe('location', () => { }); }); - describe('nearbyGPSCoordinate()', () => { - it.each( - times(100).flatMap((radius) => [ - [{ isMetric: true, radius }], - [{ isMetric: false, radius }], - ]) - )( - 'should return random gps coordinate within a distance of another one (%j)', - ({ isMetric, radius }) => { - const latitude1 = +faker.location.latitude(); - const longitude1 = +faker.location.longitude(); - - const coordinate = faker.location.nearbyGPSCoordinate({ - origin: [latitude1, longitude1], - radius, - isMetric, - }); - - expect(coordinate).toHaveLength(2); - expect(coordinate[0]).toBeTypeOf('number'); - expect(coordinate[1]).toBeTypeOf('number'); - - const latitude2 = coordinate[0]; - expect(latitude2).toBeGreaterThanOrEqual(-90.0); - expect(latitude2).toBeLessThanOrEqual(90.0); - - const longitude2 = coordinate[1]; - expect(longitude2).toBeGreaterThanOrEqual(-180.0); - expect(longitude2).toBeLessThanOrEqual(180.0); - - const actualDistance = haversine( - latitude1, - longitude1, - latitude2, - longitude2, - isMetric - ); - expect(actualDistance).toBeLessThanOrEqual(radius); - } - ); - }); + describe.each([faker, simpleFaker])( + 'nearbyGPSCoordinate()', + (fakerFn) => { + it.each( + times(100).flatMap((radius) => [ + [{ isMetric: true, radius }], + [{ isMetric: false, radius }], + ]) + )( + 'should return random gps coordinate within a distance of another one (%j)', + ({ isMetric, radius }) => { + const latitude1 = +fakerFn.location.latitude(); + const longitude1 = +fakerFn.location.longitude(); + + const coordinate = fakerFn.location.nearbyGPSCoordinate({ + origin: [latitude1, longitude1], + radius, + isMetric, + }); + + expect(coordinate).toHaveLength(2); + expect(coordinate[0]).toBeTypeOf('number'); + expect(coordinate[1]).toBeTypeOf('number'); + + const latitude2 = coordinate[0]; + expect(latitude2).toBeGreaterThanOrEqual(-90.0); + expect(latitude2).toBeLessThanOrEqual(90.0); + + const longitude2 = coordinate[1]; + expect(longitude2).toBeGreaterThanOrEqual(-180.0); + expect(longitude2).toBeLessThanOrEqual(180.0); + + const actualDistance = haversine( + latitude1, + longitude1, + latitude2, + longitude2, + isMetric + ); + expect(actualDistance).toBeLessThanOrEqual(radius); + } + ); + } + ); describe('timeZone', () => { it('should return a random timezone', () => { |
