diff --git a/src/address.ts b/src/address.ts new file mode 100644 index 00000000000..540da2060fa --- /dev/null +++ b/src/address.ts @@ -0,0 +1,482 @@ +import type { Faker } from '.'; +import type { Fake } from './fake'; +import type { Helpers } from './helpers'; + +let f: Fake['fake']; + +export class Address { + readonly Helpers: Helpers; + + constructor(private readonly faker: Faker) { + f = this.faker.fake; + this.Helpers = this.faker.helpers; + + // Bind `this` so namespaced is working correctly + for (const name of Object.getOwnPropertyNames(Address.prototype)) { + if (name === 'constructor' || typeof this[name] !== 'function') { + continue; + } + this[name] = this[name].bind(this); + } + + // TODO @Shinigami92 2022-01-13: Need better strategy + // @ts-expect-error + this.direction.schema = { + description: + 'Generates a direction. Use optional useAbbr bool to return abbreviation', + sampleResults: ['Northwest', 'South', 'SW', 'E'], + }; + // @ts-expect-error + this.cardinalDirection.schema = { + description: + 'Generates a cardinal direction. Use optional useAbbr boolean to return abbreviation', + sampleResults: ['North', 'South', 'E', 'W'], + }; + // @ts-expect-error + this.ordinalDirection.schema = { + description: + 'Generates an ordinal direction. Use optional useAbbr boolean to return abbreviation', + sampleResults: ['Northwest', 'Southeast', 'SW', 'NE'], + }; + } + + /** + * Generates random zipcode from format. If format is not specified, the + * locale's zip format is used. + * + * @method faker.address.zipCode + * @param format + */ + zipCode(format?: string) { + // if zip format is not specified, use the zip format defined for the locale + if (typeof format === 'undefined') { + const localeFormat = this.faker.definitions.address.postcode; + if (typeof localeFormat === 'string') { + format = localeFormat; + } else { + format = this.faker.random.arrayElement(localeFormat); + } + } + return this.Helpers.replaceSymbols(format); + } + + /** + * Generates random zipcode from state abbreviation. If state abbreviation is + * not specified, a random zip code is generated according to the locale's zip format. + * Only works for locales with postcode_by_state definition. If a locale does not + * have a postcode_by_state definition, a random zip code is generated according + * to the locale's zip format. + * + * @method faker.address.zipCodeByState + * @param state + */ + zipCodeByState(state: string) { + const zipRange = this.faker.definitions.address.postcode_by_state[state]; + if (zipRange) { + return this.faker.datatype.number(zipRange); + } + return this.faker.address.zipCode(); + } + + /** + * Generates a random localized city name. The format string can contain any + * method provided by faker wrapped in `{{}}`, e.g. `{{name.firstName}}` in + * order to build the city name. + * + * If no format string is provided one of the following is randomly used: + * + * * `{{address.cityPrefix}} {{name.firstName}}{{address.citySuffix}}` + * * `{{address.cityPrefix}} {{name.firstName}}` + * * `{{name.firstName}}{{address.citySuffix}}` + * * `{{name.lastName}}{{address.citySuffix}}` + * * `{{address.cityName}}` when city name is available + * + * @method faker.address.city + * @param format + */ + city(format?: string | number) { + const formats = [ + '{{address.cityPrefix}} {{name.firstName}}{{address.citySuffix}}', + '{{address.cityPrefix}} {{name.firstName}}', + '{{name.firstName}}{{address.citySuffix}}', + '{{name.lastName}}{{address.citySuffix}}', + ]; + + if (!format && this.faker.definitions.address.city_name) { + formats.push('{{address.cityName}}'); + } + + if (typeof format !== 'number') { + format = this.faker.datatype.number(formats.length - 1); + } + + return f(formats[format]); + } + + /** + * Return a random localized city prefix + * + * @method faker.address.cityPrefix + */ + cityPrefix(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.city_prefix + ); + } + + /** + * Return a random localized city suffix + * + * @method faker.address.citySuffix + */ + citySuffix(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.city_suffix + ); + } + + /** + * Returns a random city name + * + * @method faker.address.cityName + */ + cityName(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.city_name + ); + } + + /** + * Returns a random localized street name + * + * @method faker.address.streetName + */ + streetName(): string { + let result: string; + let suffix = this.faker.address.streetSuffix(); + if (suffix !== '') { + suffix = ' ' + suffix; + } + + switch (this.faker.datatype.number(1)) { + case 0: + result = this.faker.name.lastName() + suffix; + break; + case 1: + result = this.faker.name.firstName() + suffix; + break; + } + return result; + } + + // + // TODO: change all these methods that accept a boolean to instead accept an options hash. + // + + /** + * Returns a random localized street address + * + * @method faker.address.streetAddress + * @param useFullAddress + */ + streetAddress(useFullAddress: boolean = false): string { + let address = ''; + switch (this.faker.datatype.number(2)) { + case 0: + address = + this.Helpers.replaceSymbolWithNumber('#####') + + ' ' + + this.faker.address.streetName(); + break; + case 1: + address = + this.Helpers.replaceSymbolWithNumber('####') + + ' ' + + this.faker.address.streetName(); + break; + case 2: + address = + this.Helpers.replaceSymbolWithNumber('###') + + ' ' + + this.faker.address.streetName(); + break; + } + return useFullAddress + ? address + ' ' + this.faker.address.secondaryAddress() + : address; + } + + /** + * streetSuffix + * + * @method faker.address.streetSuffix + */ + streetSuffix(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.street_suffix + ); + } + + /** + * streetPrefix + * + * @method faker.address.streetPrefix + */ + streetPrefix(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.street_prefix + ); + } + + /** + * secondaryAddress + * + * @method faker.address.secondaryAddress + */ + secondaryAddress() { + return this.Helpers.replaceSymbolWithNumber( + this.faker.random.arrayElement(['Apt. ###', 'Suite ###']) + ); + } + + /** + * county + * + * @method faker.address.county + */ + county(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.county + ); + } + + /** + * country + * + * @method faker.address.country + */ + country(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.country + ); + } + + /** + * countryCode + * + * @method faker.address.countryCode + * @param alphaCode default alpha-2 + */ + countryCode(alphaCode: string = 'alpha-2'): string { + if (alphaCode === 'alpha-2') { + return this.faker.random.arrayElement( + this.faker.definitions.address.country_code + ); + } + + if (alphaCode === 'alpha-3') { + return this.faker.random.arrayElement( + this.faker.definitions.address.country_code_alpha_3 + ); + } + + return this.faker.random.arrayElement( + this.faker.definitions.address.country_code + ); + } + + /** + * state + * + * @method faker.address.state + * @param useAbbr + */ + // TODO christopher 2022-01-13: useAbbr not in use + state(useAbbr?: boolean): string { + return this.faker.random.arrayElement(this.faker.definitions.address.state); + } + + /** + * stateAbbr + * + * @method faker.address.stateAbbr + */ + stateAbbr(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.state_abbr + ); + } + + /** + * latitude + * + * @method faker.address.latitude + * @param max default is 90 + * @param min default is -90 + * @param precision default is 4 + */ + latitude(max: number = 90, min: number = -90, precision: number = 4): string { + return this.faker.datatype + .number({ + max: max, + min: min, + precision: parseFloat((0.0).toPrecision(precision) + '1'), + }) + .toFixed(precision); + } + + /** + * longitude + * + * @method faker.address.longitude + * @param max default is 180 + * @param min default is -180 + * @param precision default is 4 + */ + longitude( + max: number = 180, + min: number = -180, + precision: number = 4 + ): string { + return this.faker.datatype + .number({ + max: max, + min: min, + precision: parseFloat((0.0).toPrecision(precision) + '1'), + }) + .toFixed(precision); + } + + /** + * direction + * + * @method faker.address.direction + * @param useAbbr return direction abbreviation. defaults to false + */ + direction(useAbbr: boolean = false) { + if (!useAbbr) { + return this.faker.random.arrayElement( + this.faker.definitions.address.direction + ); + } + return this.faker.random.arrayElement( + this.faker.definitions.address.direction_abbr + ); + } + + /** + * cardinal direction + * + * @method faker.address.cardinalDirection + * @param useAbbr return direction abbreviation. defaults to false + */ + cardinalDirection(useAbbr: boolean = false): string { + if (!useAbbr) { + return this.faker.random.arrayElement( + this.faker.definitions.address.direction.slice(0, 4) + ); + } + return this.faker.random.arrayElement( + this.faker.definitions.address.direction_abbr.slice(0, 4) + ); + } + + /** + * ordinal direction + * + * @method faker.address.ordinalDirection + * @param useAbbr return direction abbreviation. defaults to false + */ + ordinalDirection(useAbbr: boolean = false): string { + if (!useAbbr) { + return this.faker.random.arrayElement( + this.faker.definitions.address.direction.slice(4, 8) + ); + } + return this.faker.random.arrayElement( + this.faker.definitions.address.direction_abbr.slice(4, 8) + ); + } + + nearbyGPSCoordinate( + coordinate?: number[], + radius?: number, + isMetric?: boolean + ): string[] { + function randomFloat(min: number, max: number): number { + return Math.random() * (max - min) + min; + } + function degreesToRadians(degrees: number): number { + return degrees * (Math.PI / 180.0); + } + function radiansToDegrees(radians: number): number { + return radians * (180.0 / Math.PI); + } + function kilometersToMiles(miles: number): number { + return miles * 0.621371; + } + function coordinateWithOffset( + coordinate: number[], + bearing: number, + distance: number, + isMetric: boolean + ): number[] { + const R = 6378.137; // Radius of the Earth (http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html) + const d = isMetric ? distance : kilometersToMiles(distance); // Distance in km + + const lat1 = degreesToRadians(coordinate[0]); //Current lat point converted to radians + const lon1 = degreesToRadians(coordinate[1]); //Current long point converted to radians + + const lat2 = Math.asin( + Math.sin(lat1) * Math.cos(d / R) + + Math.cos(lat1) * Math.sin(d / R) * Math.cos(bearing) + ); + + let lon2 = + lon1 + + Math.atan2( + Math.sin(bearing) * Math.sin(d / R) * Math.cos(lat1), + Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2) + ); + + // Keep longitude in range [-180, 180] + if (lon2 > degreesToRadians(180)) { + lon2 = lon2 - degreesToRadians(360); + } else if (lon2 < degreesToRadians(-180)) { + lon2 = lon2 + degreesToRadians(360); + } + + return [radiansToDegrees(lat2), radiansToDegrees(lon2)]; + } + + // If there is no coordinate, the best we can do is return a random GPS coordinate. + if (coordinate === undefined) { + return [this.faker.address.latitude(), this.faker.address.longitude()]; + } + radius ||= 10.0; + isMetric ||= false; + + // TODO: implement either a gaussian/uniform distribution of points in circular region. + // Possibly include param to function that allows user to choose between distributions. + + // This approach will likely result in a higher density of points near the center. + const randomCoord = coordinateWithOffset( + coordinate, + degreesToRadians(Math.random() * 360.0), + radius, + isMetric + ); + return [randomCoord[0].toFixed(4), randomCoord[1].toFixed(4)]; + } + + /** + * Return a random time zone + * + * @method faker.address.timeZone + */ + timeZone(): string { + return this.faker.random.arrayElement( + this.faker.definitions.address.time_zone + ); + } +} diff --git a/src/fake.ts b/src/fake.ts index e435ce8966e..1845dbc1070 100644 --- a/src/fake.ts +++ b/src/fake.ts @@ -79,7 +79,7 @@ export class Fake { } // assign the function from the module.function namespace - const fn = this.faker[parts[0]][parts[1]]; + const fn: Function = this.faker[parts[0]][parts[1]]; // If parameters are populated here, they are always going to be of string type // since we might actually be dealing with an object or array, diff --git a/src/helpers.ts b/src/helpers.ts index 6d6aa62fcd4..f8219b3544d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -394,7 +394,7 @@ export class Helpers { ), phone: this.faker.phone.phoneNumber(), address: { - street: this.faker.address.streetName(true), + street: this.faker.address.streetName(), suite: this.faker.address.secondaryAddress(), city: this.faker.address.city(), zipcode: this.faker.address.zipCode(), @@ -423,7 +423,7 @@ export class Helpers { username: this.faker.internet.userName(), email: this.faker.internet.email(), address: { - street: this.faker.address.streetName(true), + street: this.faker.address.streetName(), suite: this.faker.address.secondaryAddress(), city: this.faker.address.city(), zipcode: this.faker.address.zipCode(), diff --git a/src/index.ts b/src/index.ts index 65dc62257b9..16a3b3475b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { Address } from './address'; import { Animal } from './animal'; import { Commerce } from './commerce'; import { Database } from './database'; @@ -180,7 +181,7 @@ export class Faker { datatype: Datatype = new Datatype(this); - readonly address = new (require('./address'))(this); + readonly address: Address = new Address(this); readonly animal: Animal = new Animal(this); readonly commerce: Commerce = new Commerce(this); readonly company = new (require('./company'))(this);