UNPKG

@italia-tools/faker

Version:

Italian-specific fake data generator based on Faker.js

1,223 lines (1,205 loc) 46 kB
#!/usr/bin/env node #!/usr/bin/env node import { Command } from 'commander'; import { Faker, it } from '@faker-js/faker'; import { BehaviorSubject, from, of, forkJoin, lastValueFrom, Observable, combineLatest } from 'rxjs'; import { map, catchError, switchMap, mergeMap } from 'rxjs/operators'; class NameUtils { /** * Formats an Italian name to capitalize the first letter of each word * @param fullName The full name to format * @returns The formatted name */ static formatItalianName(fullName) { return fullName.toLowerCase() .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } } class LastNameSelector { constructor(faker) { this.faker = faker; this.REGIONAL_WEIGHT = 0.5; this.dataSubject = new BehaviorSubject(null); } loadRegionalData() { return from(import('./lastNamesByProvince-DK3a_fiL.js')).pipe(map(module => module.default), catchError(error => { console.error('Error loading regional data:', error); return of([]); })); } loadFallbackData() { return from(import('./lastNames-_6bErJB7.js')).pipe(map(module => module.default), catchError(error => { console.error('Error loading fallback data:', error); return of([]); })); } initializeMaps() { if (this.dataSubject.getValue()) { return of(undefined); } return forkJoin({ regionalData: this.loadRegionalData(), fallbackData: this.loadFallbackData() }).pipe(map(({ regionalData, fallbackData }) => { const regionMap = new Map(); const provinceMap = new Map(); regionalData.forEach(data => { if (!regionMap.has(data.region)) { regionMap.set(data.region, []); } regionMap.get(data.region)?.push(data); provinceMap.set(data.province.toLowerCase(), data); }); this.dataSubject.next({ regionMap, provinceMap, fallbackSurnames: fallbackData }); })); } select(options) { return this.initializeMaps().pipe(switchMap(() => { const data = this.dataSubject.getValue(); if (!data) { throw new Error('Data not initialized'); } if (!options) { return of(this.faker.helpers.arrayElement(data.fallbackSurnames)); } let localSurnames = []; if (options.province) { const provinceData = data.provinceMap.get(options.province.toLowerCase()); if (provinceData) { localSurnames = provinceData.surnames; } } else if (options.region) { const regionData = data.regionMap.get(options.region); if (regionData) { localSurnames = regionData.flatMap(data => data.surnames); } } if (localSurnames.length === 0) { return of(this.faker.helpers.arrayElement(data.fallbackSurnames)); } const useLocal = this.faker.number.float({ min: 0, max: 1 }) < this.REGIONAL_WEIGHT; if (useLocal) { return of(this.faker.helpers.arrayElement(localSurnames)); } else { return of(this.faker.helpers.arrayElement(data.fallbackSurnames)); } })); } preloadData() { return this.initializeMaps(); } clearCache() { this.dataSubject.next(null); } } // Example usage: /* const lastNameSelector = new LastNameSelector(faker); // Select a lastname lastNameSelector.select({ region: 'Lombardia' }).subscribe( lastname => console.log('Selected lastname:', lastname), error => console.error('Error:', error) ); // Preload data lastNameSelector.preloadData().subscribe( () => console.log('Data preloaded'), error => console.error('Error preloading:', error) ); */ class LastNameModule { constructor(faker) { this.faker = faker; this.lastNameSelector = new LastNameSelector(faker); } lastName$(options) { return this.lastNameSelector.select(options).pipe(map(name => NameUtils.formatItalianName(name))); } preloadData$() { return this.lastNameSelector.preloadData(); } async lastName(options) { return lastValueFrom(this.lastName$(options)); } async preloadData() { return lastValueFrom(this.preloadData$()); } clearCache() { this.lastNameSelector.clearCache(); } } class WeightedRandomSelector { /** * Creates a WeightedRandomSelector that respects actual weights * @param items Array of items with weights * @param isSorted boolean indicating if items are pre-sorted */ constructor(items, isSorted = false) { this.SMALL_DATASET_THRESHOLD = 100; this.totalWeight = 0; if (items.length === 0) { throw new Error("Items array cannot be empty"); } this.isSmallDataset = items.length <= this.SMALL_DATASET_THRESHOLD; if (this.isSmallDataset) { this.initializeSmallDataset(items); } else { this.initializeLargeDataset(items, isSorted); } } initializeSmallDataset(items) { this.fastAccessArray = []; items.forEach(item => { // For small datasets, we keep direct representation this.fastAccessArray.push(...Array(item.weight).fill(item.value)); }); } /** * Initialization for large datasets using cumulative weights */ initializeLargeDataset(items, isSorted) { this.items = []; let accumulatedWeight = 0; // If not sorted, sort by descending weight to optimize search const workingItems = isSorted ? items : [...items].sort((a, b) => b.weight - a.weight); workingItems.forEach(item => { accumulatedWeight += item.weight; this.items.push({ value: item.value, weight: item.weight, accumulatedWeight }); }); this.totalWeight = accumulatedWeight; } /** * Returns a random item respecting actual weights */ select() { if (this.isSmallDataset && this.fastAccessArray) { return this.fastAccessArray[Math.floor(Math.random() * this.fastAccessArray.length)]; } const randomWeight = Math.random() * this.totalWeight; // Binary search to find element with correct cumulative weight return this.binarySearch(randomWeight); } binarySearch(targetWeight) { let left = 0; let right = this.items.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); const item = this.items[mid]; if (mid === 0 || (this.items[mid - 1].accumulatedWeight <= targetWeight && item.accumulatedWeight > targetWeight)) { return item.value; } if (item.accumulatedWeight <= targetWeight) { left = mid + 1; } else { right = mid - 1; } } // Fallback to first element (this should never happen) return this.items[0].value; } selectMultiple(count) { return Array.from({ length: count }, () => this.select()); } } var Gender; (function (Gender) { Gender["Male"] = "male"; Gender["Female"] = "female"; })(Gender || (Gender = {})); class FirstNameModule { constructor(faker) { this.faker = faker; this.commonTitles = { male: ['Dott.', 'Ing.', 'Avv.', 'Prof.', 'Arch.', 'Rag.'], female: ['Dott.ssa', 'Ing.', 'Avv.', 'Prof.ssa', 'Arch.', 'Rag.'], neutral: ['Ing.', 'Avv.', 'Arch.', 'Rag.', 'Geom.'] }; this.dataSubject = new BehaviorSubject(null); } loadNameData() { if (this.dataSubject.getValue()) { return of(undefined); } const femaleNamesPromise = import('./femaleFirstNames-5GW1cLTt.js'); const maleNamesPromise = import('./maleFirstNames-CvboZVmW.js'); return from(Promise.all([femaleNamesPromise, maleNamesPromise])).pipe(map(([femaleModule, maleModule]) => { const femaleFirstNames = femaleModule.default; const maleFirstNames = maleModule.default; const femaleNamesSelector = new WeightedRandomSelector(femaleFirstNames.items); const maleNamesSelector = new WeightedRandomSelector(maleFirstNames.items); this.dataSubject.next({ femaleNamesSelector, maleNamesSelector }); }), catchError(error => { console.error('Error loading name data:', error); throw error; })); } firstName$(options) { return this.loadNameData().pipe(switchMap(() => { const data = this.dataSubject.getValue(); if (!data) { throw new Error('Name data not initialized'); } const gender = options?.gender || this.faker.helpers.arrayElement([Gender.Male, Gender.Female]); let name = gender === Gender.Male ? data.maleNamesSelector.select() : data.femaleNamesSelector.select(); name = NameUtils.formatItalianName(name); if (options?.prefix) { return of(this.getNameWithPrefix(name, gender)); } return of(name); }), catchError(error => { console.error('Error generating first name:', error); return of(''); })); } prefix$(gender) { return of(this.getPrefix(gender)); } preloadData$() { return this.loadNameData(); } async firstName(options) { return lastValueFrom(this.firstName$(options)); } async prefix(gender) { return lastValueFrom(this.prefix$(gender)); } async preloadData() { return lastValueFrom(this.preloadData$()); } getNameWithPrefix(name, gender) { return `${this.getPrefix(gender)} ${name}`; } getPrefix(gender) { if (!gender) { return this.faker.helpers.arrayElement(this.commonTitles.neutral); } return this.faker.helpers.arrayElement(this.commonTitles[gender]); } clearCache() { this.dataSubject.next(null); } } class CityAdapter { /** * Converts raw city data to ItalianCity * Used for deserialization * @param raw RawItalianCity * @returns ItalianCity * */ static toEnglish(raw) { return { name: raw.nome, code: raw.codice, zone: { code: raw.zona.codice, name: raw.zona.nome }, region: { code: raw.regione.codice, name: raw.regione.nome }, province: { code: raw.provincia.codice, name: raw.provincia.nome }, provinceCode: raw.sigla, belfioreCode: raw.codiceCatastale, postalCodes: raw.cap, population: raw.popolazione }; } /** * Converts ItalianCity to raw city data * Used for serialization * @param city ItalianCity * @returns RawItalianCity * */ static toRaw(city) { return { nome: city.name, codice: city.code, zona: { codice: city.zone.code, nome: city.zone.name }, regione: { codice: city.region.code, nome: city.region.name }, provincia: { codice: city.province.code, nome: city.province.name }, sigla: city.provinceCode, codiceCatastale: city.belfioreCode, cap: city.postalCodes, popolazione: city.population }; } /** * Converts city data for use with WeightedRandomSelector * Uses population as weight */ static toWeightedItems(cities) { return cities.map(city => ({ value: city, weight: city.popolazione })); } /** * Converts city data with custom formatting */ static toFormattedWeightedItems(cities, options = {}) { return cities.map(city => ({ value: CityAdapter.formatCity(city, options), weight: city.popolazione })); } /** * Formats city data as a string */ static formatCity(city, options) { const parts = [city.nome]; if (options.includeProvince) { parts.push(`(${city.sigla})`); } if (options.includeCap) { parts.push(city.cap[0]); } if (options.includeRegion) { parts.push(city.regione.nome); } return parts.join(' '); } } // biome-ignore lint/complexity/noStaticOnlyClass: <explanation> class CountryAdapter { /** * Converts raw country data to Country * Used for deserialization */ static toEnglish(raw) { return { continent: raw.denominazioneContinente, istatCode: raw.codiceIstat, nameIt: raw.denominazioneIt, nameEn: raw.denominazioneEn, minCode: raw.codiceMin, atCode: raw.codiceAt, unsdm49Code: raw.codiceUnsdm49, iso3166Alpha2: raw.codiceIso3166Alpha2, iso3166Alpha3: raw.codiceIso3166Alpha3 }; } /** * Converts Country to raw country data * Used for serialization */ static toRaw(country) { return { denominazioneContinente: country.continent, codiceIstat: country.istatCode, denominazioneIt: country.nameIt, denominazioneEn: country.nameEn, codiceMin: country.minCode, codiceAt: country.atCode, codiceUnsdm49: country.unsdm49Code, codiceIso3166Alpha2: country.iso3166Alpha2, codiceIso3166Alpha3: country.iso3166Alpha3 }; } } class PlacesModule { constructor(faker) { this.faker = faker; this.dataSubject = new BehaviorSubject(null); this.countrySubject = new BehaviorSubject(null); } loadCityData() { if (this.dataSubject.getValue()) { return of(undefined); } return from(import('./cities-C_uQCy18.js')).pipe(map(module => { const citiesData = module.default; const citySelector = new WeightedRandomSelector(CityAdapter.toWeightedItems(citiesData)); this.dataSubject.next({ citySelector, citiesData }); }), catchError(error => { console.error('Error loading city data:', error); throw error; })); } loadCountryData() { // Check if data is already loaded const existingData = this.countrySubject.getValue(); if (existingData) { return of(undefined); } return from(import('./countries-D9vX0Ja_.js')).pipe(map(module => { const countries = module.default.map((country) => CountryAdapter.toEnglish(country)); this.countrySubject.next(countries); }), catchError(error => { console.error('Error loading countries:', error); throw new Error(`Failed to load country data: ${error.message}`); })); } randomCity$() { return this.loadCityData().pipe(map(() => this.selectItalianCity())); } randomCities$(count) { return this.loadCityData().pipe(switchMap(() => { const uniqueCities = new Set(); return new Observable(observer => { while (uniqueCities.size < count) { uniqueCities.add(this.selectItalianCity()); } observer.next(Array.from(uniqueCities)); observer.complete(); }); })); } province$() { return this.randomCity$().pipe(map(randomCity => ({ name: randomCity.province.name, code: randomCity.code }))); } city$(options) { return this.loadCityData().pipe(map(() => { const data = this.dataSubject.getValue(); if (!data) { throw new Error('City data not initialized'); } // Search by belfioreCode (exact match) if (options?.belfioreCode) { const city = data.citiesData.find(city => city.codiceCatastale.toLowerCase() === options.belfioreCode?.toLowerCase()); return city ? CityAdapter.toEnglish(city) : null; } // Search by name (exact match or case-insensitive) if (options?.cityName) { const city = data.citiesData.find(city => city.nome.toLowerCase() === options.cityName?.toLowerCase()); return city ? CityAdapter.toEnglish(city) : null; } if (options?.province) { const filteredCities = data.citiesData.filter(city => city.provincia.nome.toLowerCase() === options.province?.toLowerCase()); if (filteredCities.length > 0) { return CityAdapter.toEnglish(this.faker.helpers.arrayElement(filteredCities)); } } if (options?.region) { const filteredCities = data.citiesData.filter(city => city.regione.nome.toLowerCase() === options.region?.toLowerCase()); if (filteredCities.length > 0) { return CityAdapter.toEnglish(this.faker.helpers.arrayElement(filteredCities)); } } return this.selectItalianCity(); })); } allCities$() { return this.loadCityData().pipe(map(() => { const data = this.dataSubject.getValue(); if (!data) { throw new Error('City data not initialized'); } return data.citiesData.map(city => CityAdapter.toEnglish(city)); })); } mostPopulatedCities$(x) { return this.loadCityData().pipe(map(() => { const data = this.dataSubject.getValue(); if (!data) { throw new Error('City data not initialized'); } const cities = data.citiesData.map(city => CityAdapter.toEnglish(city)); return cities.sort((a, b) => b.population - a.population).slice(0, x); })); } ; getBirthPlace$() { return this.randomCity$().pipe(map(randomCity => ({ name: randomCity.name, belfioreCode: randomCity.belfioreCode, province: randomCity.province.name, region: randomCity.region.name, provinceCode: randomCity.provinceCode }))); } region$() { return this.randomCity$().pipe(map(randomCity => randomCity.region.name)); } preloadData$() { return this.loadCityData(); } randomCountry$() { return this.loadCountryData().pipe(map(() => { const countries = this.countrySubject.getValue(); if (!countries) throw new Error('Country data not initialized'); return this.faker.helpers.arrayElement(countries); })); } getCountryByName$(name) { if (!name || typeof name !== 'string') { return of(null); } return this.loadCountryData().pipe(map(() => { const countries = this.countrySubject.getValue(); if (!countries || !Array.isArray(countries)) { throw new Error('Country data not properly initialized'); } return countries.find(country => country.nameIt.toLowerCase() === name.toLowerCase() || country.nameEn.toLowerCase() === name.toLowerCase()) || null; }), catchError(error => { console.error('Error in getCountryByName$:', error); return of(null); })); } getAllCountries$() { return this.loadCountryData().pipe(map(() => { const countries = this.countrySubject.getValue(); if (!countries) throw new Error('Country data not initialized'); return countries; })); } async randomCity() { return lastValueFrom(this.randomCity$()); } async randomCities(count) { return lastValueFrom(this.randomCities$(count)); } async province() { return lastValueFrom(this.province$()); } async city(options) { return lastValueFrom(this.city$(options)); } async allCities() { return lastValueFrom(this.allCities$()); } async mostPopulatedCities(x) { return lastValueFrom(this.mostPopulatedCities$(x)); } async getBirthPlace() { return lastValueFrom(this.getBirthPlace$()); } async region() { return lastValueFrom(this.region$()); } async preloadData() { return lastValueFrom(this.preloadData$()); } async randomCountry() { return lastValueFrom(this.randomCountry$()); } async getCountryByName(name) { return lastValueFrom(this.getCountryByName$(name)); } async getAllCountries() { return lastValueFrom(this.getAllCountries$()); } clearCache() { this.dataSubject.next(null); this.countrySubject.next(null); } selectItalianCity() { const data = this.dataSubject.getValue(); if (!data) { throw new Error('City data not initialized'); } const randomCity = data.citySelector.select(); return CityAdapter.toEnglish(randomCity); } } // biome-ignore lint/complexity/noStaticOnlyClass: <explanation> class FiscalCodeGenerator { static generate(person) { const surname = FiscalCodeGenerator.processSurname(person.lastName); const name = FiscalCodeGenerator.processName(person.firstName); const dateCode = FiscalCodeGenerator.processDate(person.birthDate, person.gender); const belfioreCode = person.birthPlace.type === 'italian' ? person.birthPlace.city.belfioreCode : person.birthPlace.country.code; const baseCode = surname + name + dateCode + belfioreCode; const controlChar = FiscalCodeGenerator.calculateControlChar(baseCode); return baseCode + controlChar; } static processSurname(surname) { const consonants = surname.toUpperCase().replace(/[^BCDFGHJKLMNPQRSTVWXYZ]/g, ''); const vowels = surname.toUpperCase().replace(/[^AEIOU]/g, ''); const combined = consonants + vowels + 'XXX'; return combined.slice(0, 3); } static processName(name) { const consonants = name.toUpperCase().replace(/[^BCDFGHJKLMNPQRSTVWXYZ]/g, ''); if (consonants.length > 3) { return consonants[0] + consonants[2] + consonants[3]; } const vowels = name.toUpperCase().replace(/[^AEIOU]/g, ''); const combined = consonants + vowels + 'XXX'; return combined.slice(0, 3); } static processDate(date, gender) { const year = date.getFullYear().toString().slice(-2); const month = FiscalCodeGenerator.MONTH_CODES[date.getMonth()]; let day = date.getDate().toString().padStart(2, '0'); if (gender === Gender.Female) { day = (parseInt(day) + 40).toString(); } return year + month + day; } static calculateControlChar(code) { if (code.length !== 15) { throw new Error('Base code must be exactly 15 characters long'); } let sum = 0; // Process characters in odd positions (1-based index) for (let i = 0; i < code.length; i += 2) { const char = code[i]; const value = FiscalCodeGenerator.ODD_CHARS[char]; if (value === undefined) { throw new Error(`Invalid character in odd position: ${char}`); } sum += value; } // Process characters in even positions (1-based index) for (let i = 1; i < code.length; i += 2) { const char = code[i]; const value = FiscalCodeGenerator.EVEN_CHARS[char]; if (value === undefined) { throw new Error(`Invalid character in even position: ${char}`); } sum += value; } // Calculate remainder and get corresponding letter const remainder = sum % 26; return FiscalCodeGenerator.REMAINDER_CHARS.charAt(remainder); } } FiscalCodeGenerator.MONTH_CODES = ['A', 'B', 'C', 'D', 'E', 'H', 'L', 'M', 'P', 'R', 'S', 'T']; FiscalCodeGenerator.ODD_CHARS = { '0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21, 'A': 1, 'B': 0, 'C': 5, 'D': 7, 'E': 9, 'F': 13, 'G': 15, 'H': 17, 'I': 19, 'J': 21, 'K': 2, 'L': 4, 'M': 18, 'N': 20, 'O': 11, 'P': 3, 'Q': 6, 'R': 8, 'S': 12, 'T': 14, 'U': 16, 'V': 10, 'W': 22, 'X': 25, 'Y': 24, 'Z': 23 }; FiscalCodeGenerator.EVEN_CHARS = { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'J': 9, 'K': 10, 'L': 11, 'M': 12, 'N': 13, 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20, 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25 }; FiscalCodeGenerator.REMAINDER_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; class FiscalCodeModule { constructor(faker) { this.faker = faker; this.placesModule = new PlacesModule(faker); this.firstNameModule = new FirstNameModule(faker); this.lastNameModule = new LastNameModule(faker); } generate$(options) { const gender = options?.gender ?? this.faker.helpers.arrayElement([Gender.Male, Gender.Female]); const birthDate = options?.birthDate ?? this.faker.date.birthdate(); const firstName$ = options?.firstName ? of(options.firstName) : this.firstNameModule.firstName$(); const lastName$ = options?.lastName ? of(options.lastName) : this.lastNameModule.lastName$(); const birthPlace$ = options?.birthPlace ? this.validateBirthPlace(options.birthPlace) : this.placesModule.randomCity$().pipe(map(city => ({ type: 'italian', city }))); return forkJoin({ firstName: firstName$, lastName: lastName$, birthPlace: birthPlace$ }).pipe(map(({ firstName, lastName, birthPlace }) => FiscalCodeGenerator.generate({ firstName, lastName, gender, birthDate, birthPlace }))); } async generate(options) { return lastValueFrom(this.generate$(options)); } validateBirthPlace(birthPlace) { if (birthPlace.type === 'italian' && birthPlace.city && birthPlace.city.belfioreCode) { return this.placesModule.city$({ belfioreCode: birthPlace.city.belfioreCode }).pipe(map(city => { if (!city) throw new Error('Invalid Belfiore Code'); return { type: 'italian', city }; })); } if (birthPlace.type === 'italian' && birthPlace.city && birthPlace.city.name) { return this.placesModule.city$({ cityName: birthPlace.city.name }).pipe(map(city => { if (!city) throw new Error('Invalid city name'); return { type: 'italian', city }; })); } if (birthPlace.type === 'foreign' && birthPlace.country?.name) { return this.placesModule.getCountryByName$(birthPlace.country.name).pipe(map(country => { if (!country) throw new Error(`Invalid country name: ${birthPlace.country.name}`); const countryDto = { name: country.nameEn, code: country.atCode }; return { type: 'foreign', country: countryDto }; })); } throw new Error('Invalid birth place'); } } class AddressModule { constructor(faker) { this.faker = faker; // Street types with weighted probabilities this.streetTypes = [ { value: 'Via', weight: 70 }, // Most common { value: 'Viale', weight: 8 }, // Fairly common { value: 'Piazza', weight: 8 }, // Fairly common { value: 'Corso', weight: 5 }, // Less common { value: 'Largo', weight: 3 }, // Rare { value: 'Vicolo', weight: 2 }, // Very rare { value: 'Lungomare', weight: 1 }, // Very rare { value: 'Strada', weight: 1 }, // Very rare { value: 'Salita', weight: 0.5 }, // Extremely rare { value: 'Calata', weight: 0.5 }, // Extremely rare { value: 'Galleria', weight: 0.5 }, // Extremely rare { value: 'Borgo', weight: 0.3 }, // Extremely rare { value: 'Traversa', weight: 0.2 } // Extremely rare ]; this.historicalFigures = [ 'Giuseppe Garibaldi', 'Giuseppe Mazzini', 'Vittorio Emanuele', 'Dante Alighieri', 'Leonardo da Vinci', 'Cristoforo Colombo', 'Alessandro Manzoni', 'Galileo Galilei' ]; this.saints = [ 'San Francesco', 'San Giovanni', 'Santa Maria', 'San Giuseppe', 'Sant\'Antonio', 'San Marco', 'Santa Chiara', 'San Pietro' ]; this.importantDates = [ 'XX Settembre', 'IV Novembre', 'XXV Aprile', 'II Giugno', 'I Maggio', 'VIII Agosto' ]; this.culturalReferences = [ 'Roma', 'Venezia', 'Milano', 'Napoli', // Common city-named streets 'dei Mille', 'delle Grazie', 'della Repubblica', 'della Libertà', 'della Costituzione', 'della Pace', 'dell\'Indipendenza', 'dell\'Unità', 'Verdi', 'Rossini', 'Puccini' ]; this.geographicalReferences = [ 'Monte Bianco', 'Vesuvio', 'Etna', 'Dolomiti', 'Tevere', 'Po', 'Arno', 'Mediterraneo' ]; this.placesModule = new PlacesModule(faker); this.lastNameModule = new LastNameModule(faker); } getWeightedStreetType() { const totalWeight = this.streetTypes.reduce((sum, type) => sum + type.weight, 0); let random = this.faker.number.float({ min: 0, max: totalWeight }); for (const streetType of this.streetTypes) { random -= streetType.weight; if (random <= 0) { return streetType.value; } } return 'Via'; // Fallback to most common } streetName$(region) { const basePatterns = [ ...this.historicalFigures, ...this.saints, ...this.importantDates, ...this.culturalReferences, ...this.geographicalReferences ]; if (!region) { return of(this.faker.helpers.arrayElement(basePatterns)); } return this.lastNameModule.lastName$({ region }).pipe(map(regionalFigure => { const allPatterns = [...basePatterns, regionalFigure]; return this.faker.helpers.arrayElement(allPatterns); })); } streetAddress$(options) { return this.streetName$(options?.region).pipe(map(name => { const streetType = this.getWeightedStreetType(); const buildingNumber = this.buildingNumber(); return `${streetType} ${name}, ${buildingNumber}`; })); } completeAddress$(options) { return this.streetAddress$(options).pipe(switchMap(streetAddr => (options?.region ? this.placesModule.city$({ region: options.region }) : this.placesModule.randomCity$()).pipe(map(city => { // Only 5% chance to include apartment details const includeApartmentDetails = this.faker.helpers.maybe(() => true, { probability: 0.05 }); const apartment = includeApartmentDetails ? this.generateApartmentDetails() : ''; const cap = city?.postalCodes[0] || this.faker.string.numeric(5); const baseAddress = [streetAddr, apartment].filter(Boolean).join(' '); return `${baseAddress}, ${cap} ${city?.name} (${city?.provinceCode})`; })))); } buildingNumber() { const number = this.faker.number.int({ min: 1, max: 300 }); const suffix = this.faker.helpers.maybe(() => this.faker.helpers.arrayElement(['A', 'B', '/a', '/b', 'bis']), { probability: 0.05 } // Reduced probability for number suffixes ); return suffix ? `${number}${suffix}` : `${number}`; } generateApartmentDetails() { const buildings = ['A', 'B', 'C']; const floor = this.faker.number.int({ min: 0, max: 8 }); const apartmentNumber = this.faker.number.int({ min: 1, max: 15 }); const internalLetter = this.faker.helpers.maybe(() => this.faker.helpers.arrayElement(['a', 'b']), { probability: 0.2 }); return `Scala ${this.faker.helpers.arrayElement(buildings)}, Piano ${floor}, Interno ${apartmentNumber}${internalLetter || ''}`; } async streetName(region) { return lastValueFrom(this.streetName$(region)); } async streetAddress(options) { return lastValueFrom(this.streetAddress$(options)); } async completeAddress(options) { return lastValueFrom(this.completeAddress$(options)); } } class PersonModule { constructor(faker) { this.faker = faker; this.lastNameModule = new LastNameModule(faker); this.namesModule = new FirstNameModule(faker); this.placesModule = new PlacesModule(faker); this.fiscalCodeModule = new FiscalCodeModule(faker); this.addressModule = new AddressModule(faker); } firstName$(options) { return this.namesModule.firstName$(options); } lastName$(options) { return this.lastNameModule.lastName$(options); } fullName$(options) { return forkJoin({ first: this.firstName$(options), last: this.lastName$() }).pipe(map(({ first, last }) => `${first} ${last}`)); } prefix$(gender) { return this.namesModule.prefix$(gender); } fiscalCode$(options) { return this.fiscalCodeModule.generate$(options); } birthPlace$() { return this.placesModule.randomCity$().pipe(map(city => city.name)); } province$() { return this.placesModule.province$(); } birthDate$(minAge, maxAge) { if (minAge !== undefined && maxAge !== undefined) { const currentYear = new Date().getFullYear(); const fromYear = currentYear - maxAge; const toYear = currentYear - minAge; const fromDate = new Date(`${fromYear}-01-01`); const toDate = new Date(`${toYear}-12-31`); const currentDate = new Date(); if (toYear === currentYear) { toDate.setFullYear(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()); } return of(this.faker.date.between({ from: fromDate, to: toDate })); } return of(this.faker.date.past()); } phone$() { return of(this.generatePhone()); } landline$() { return of(this.generateLandline()); } email$(firstName, lastName) { return forkJoin({ first: firstName ? of(firstName) : this.firstName$(), last: lastName ? of(lastName) : this.lastName$() }).pipe(map(({ first, last }) => this.generateEmail(first, last))); } pec$(firstName, lastName) { return forkJoin({ first: firstName ? of(firstName) : this.firstName$(), last: lastName ? of(lastName) : this.lastName$() }).pipe(map(({ first, last }) => this.generatePec(first, last))); } generatePerson$(options) { // Default values for basic personal information const gender = options?.gender || this.faker.helpers.arrayElement([Gender.Male, Gender.Female]); const birthDate = this.faker.date.birthdate({ mode: 'age', min: options?.minAge || 18, max: options?.maxAge || 80 }); // Build location options based on provided filters const locationOptions = options?.province ? { province: options.province } : options?.region ? { region: options.region } : undefined; const nameOptions = { region: options?.region, province: options?.province }; return this.placesModule.city$(locationOptions).pipe( // First stage: Generate basic personal data mergeMap(birthCity => combineLatest({ firstName: this.firstName$({ gender }), lastName: this.lastName$(nameOptions), prefix: options?.withTitle ? this.prefix$(gender) : of(''), birthPlace: of(birthCity) })), // Second stage: Generate documents and contact details mergeMap(({ firstName, lastName, prefix, birthPlace }) => combineLatest({ base: of({ firstName, lastName, prefix, birthPlace }), fiscalCode: this.fiscalCodeModule.generate$({ firstName, lastName, gender, birthDate, birthPlace: { type: 'italian', city: birthPlace } }), email: this.email$(firstName, lastName), pec: this.pec$(firstName, lastName), address: this.addressModule.completeAddress$() })), // Final stage: Compose complete person profile map(({ base, fiscalCode, email, pec, address }) => ({ fullName: [base.prefix, base.firstName, base.lastName].filter(Boolean).join(' '), firstName: base.firstName, lastName: base.lastName, gender, birthDate, birthPlace: { city: base.birthPlace?.name ?? '', province: base.birthPlace?.province.name ?? '', region: base.birthPlace?.region.name ?? '' }, fiscalCode, contacts: { phone: this.generatePhone(), email, pec }, address, }))); } async firstName(options) { return lastValueFrom(this.firstName$(options)); } async lastName(options) { return lastValueFrom(this.lastName$(options)); } async fullName(options) { return lastValueFrom(this.fullName$(options)); } async fiscalCode(options) { return lastValueFrom(this.fiscalCode$(options)); } async email(firstName, lastName) { return lastValueFrom(this.email$(firstName, lastName)); } async pec(firstName, lastName) { return lastValueFrom(this.pec$(firstName, lastName)); } async generatePerson(options) { return lastValueFrom(this.generatePerson$(options)); } generatePhone() { const prefixes = ['320', '328', '338', '348', '350', '360', '368', '388', '389', '391', '392']; const prefix = this.faker.helpers.arrayElement(prefixes); const number = this.faker.string.numeric(7); return `${prefix}${number}`; } generateLandline() { const prefixes = ['02', '06', '010', '011', '045', '051']; const prefix = this.faker.helpers.arrayElement(prefixes); const number = this.faker.string.numeric(7); return `${prefix}${number}`; } generateEmail(firstName, lastName) { const first = firstName.toLowerCase().replace(/ /g, '.'); const last = lastName.toLowerCase().replace(/ /g, '.'); const domains = ['gmail.com', 'yahoo.it', 'libero.it', 'hotmail.it', 'outlook.it']; const domain = this.faker.helpers.arrayElement(domains); const separator = this.faker.helpers.arrayElement(['.', '_', '']); return `${first}${separator}${last}@${domain}`; } generatePec(firstName, lastName) { const first = firstName.toLowerCase().replace(/ /g, '.'); const last = lastName.toLowerCase().replace(/ /g, '.'); const domains = ['pec.it', 'legalmail.it', 'pecmail.it']; const domain = this.faker.helpers.arrayElement(domains); return `${first}.${last}@${domain}`; } parseGender(value) { const isValidGender = value === Gender.Male || value === Gender.Female; return isValidGender ? value : undefined; } } class ItFaker extends Faker { constructor() { super({ locale: [it] }); } get itPerson() { if (!this._itPerson) { this._itPerson = new PersonModule(this); } return this._itPerson; } get itPlace() { if (!this._itPlace) { this._itPlace = new PlacesModule(this); } return this._itPlace; } get itAddress() { if (!this._itAddress) { this._itAddress = new AddressModule(this); } return this._itAddress; } get itFiscalCode() { if (!this._itFiscalCode) { this._itFiscalCode = new FiscalCodeModule(this); } return this._itFiscalCode; } get itLastName() { if (!this._itLastName) { this._itLastName = new LastNameModule(this); } return this._itLastName; } get itFirstName() { if (!this._itFirstName) { this._itFirstName = new LastNameModule(this); } return this._itFirstName; } } const faker = new ItFaker(); // Utility function per validare il numero function parseInteger(value) { const parsedValue = Number.parseInt(value, 10); if (Number.isNaN(parsedValue)) { throw new Error('Value must be a number'); } return parsedValue; } // Utility function per formattare l'output function formatOutput(data) { return JSON.stringify(data, null, 2); } const program = new Command(); program .name('it-faker') .description('Generate authentic Italian fake data') .version('0.1.0-alpha.2'); program.command('person') .description('Generate Italian person data') .option('-r, --region <region>', 'Specify Italian region') .option('-p, --province <province>', 'Specify Italian province') .option('-g, --gender <gender>', 'Specify gender (male/female)') .option('-c, --count <number>', 'Number of persons to generate', '1') .option('--with-address', 'Include address') .action(async (options) => { try { const count = parseInteger(options.count); if (count < 1) { console.error('Count must be greater than 0'); process.exit(1); } const persons = await Promise.all(Array(count).fill(null).map(async () => { const person = await faker.itPerson.generatePerson({ gender: options.gender, region: options.region, province: options.province }); if (options.withAddress) { const address = await faker.itAddress.completeAddress({ region: options.region, province: options.province }); return { ...person, address }; } return person; })); console.log(formatOutput(count === 1 ? persons[0] : persons)); } catch (error) { console.error('Error generating person data:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); program.command('address') .description('Generate Italian address') .option('-r, --region <region>', 'Specify Italian region') .option('-p, --province <province>', 'Specify Italian province') .option('-c, --count <number>', 'Number of addresses to generate', '1') .action(async (options) => { try { const count = parseInteger(options.count); if (count < 1) { console.error('Count must be greater than 0'); process.exit(1); } const addresses = await Promise.all(Array(count).fill(null).map(() => faker.itAddress.completeAddress({ region: options.region, province: options.province }))); console.log(formatOutput(count === 1 ? addresses[0] : addresses)); } catch (error) { console.error('Error generating address:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); program.command('fiscal-code') .description('Generate Italian fiscal code') .option('-c, --count <number>', 'Number of fiscal codes to generate', '1') .action(async (options) => { try { const count = parseInteger(options.count); if (count < 1) { console.error('Count must be greater than 0'); process.exit(1); } const codes = await Promise.all(Array(count).fill(null).map(() => faker.itFiscalCode.generate())); console.log(formatOutput(count === 1 ? codes[0] : codes)); } catch (error) { console.error('Error generating fiscal code:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Mostra help se non vengono forniti argomenti if (process.argv.length === 2) { program.help(); } program.parse();