bg-name-days 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/package.json +41 -0
- package/src/data/april.js +135 -0
- package/src/data/august.js +175 -0
- package/src/data/december.js +265 -0
- package/src/data/february.js +125 -0
- package/src/data/january.js +255 -0
- package/src/data/july.js +125 -0
- package/src/data/june.js +195 -0
- package/src/data/march.js +135 -0
- package/src/data/may.js +155 -0
- package/src/data/nameDays.js +132 -0
- package/src/data/november.js +225 -0
- package/src/data/october.js +165 -0
- package/src/data/september.js +215 -0
- package/src/data/transliteration-map.js +58 -0
- package/src/index.js +20 -0
- package/src/lookup.js +137 -0
- package/src/moveable.js +221 -0
- package/src/normalize.js +60 -0
- package/src/search.js +78 -0
- package/src/transliterate.js +56 -0
- package/src/types.js +58 -0
- package/src/upcoming.js +64 -0
package/src/normalize.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').NameDayEntry} NameDayEntry */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a string for case-insensitive matching.
|
|
5
|
+
* Trims whitespace and lowercases.
|
|
6
|
+
* @param {string} str
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function normalize(str) {
|
|
10
|
+
if (!str || typeof str !== 'string') return '';
|
|
11
|
+
return str.trim().toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format a Date or "MM-DD" string to "MM-DD".
|
|
16
|
+
* @param {string|Date} date
|
|
17
|
+
* @returns {string|null}
|
|
18
|
+
*/
|
|
19
|
+
export function formatDate(date) {
|
|
20
|
+
if (!date) return null;
|
|
21
|
+
|
|
22
|
+
if (typeof date === 'string') {
|
|
23
|
+
// Validate "MM-DD" format
|
|
24
|
+
if (/^\d{2}-\d{2}$/.test(date)) return date;
|
|
25
|
+
// Try to parse as Date string
|
|
26
|
+
const parsed = new Date(date);
|
|
27
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
28
|
+
return pad(parsed.getMonth() + 1) + '-' + pad(parsed.getDate());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (date instanceof Date) {
|
|
32
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
33
|
+
return pad(date.getMonth() + 1) + '-' + pad(date.getDate());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract year from a Date.
|
|
41
|
+
* @param {string|Date} date
|
|
42
|
+
* @returns {number}
|
|
43
|
+
*/
|
|
44
|
+
export function extractYear(date) {
|
|
45
|
+
if (date instanceof Date) return date.getFullYear();
|
|
46
|
+
if (typeof date === 'string') {
|
|
47
|
+
const parsed = new Date(date);
|
|
48
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.getFullYear();
|
|
49
|
+
}
|
|
50
|
+
return new Date().getFullYear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pad a number to 2 digits.
|
|
55
|
+
* @param {number} n
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function pad(n) {
|
|
59
|
+
return String(n).padStart(2, '0');
|
|
60
|
+
}
|
package/src/search.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').SearchResult} SearchResult */
|
|
2
|
+
|
|
3
|
+
import { getIndex } from './data/nameDays.js';
|
|
4
|
+
import { normalize } from './normalize.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Search names by prefix. Searches primary names, variants, and Latin transliterations.
|
|
8
|
+
* Case-insensitive. Returns unique SearchResult objects.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} prefix - Prefix to search for (Cyrillic or Latin)
|
|
11
|
+
* @param {number} [year] - Year for moveable dates (default: current year)
|
|
12
|
+
* @returns {SearchResult[]}
|
|
13
|
+
*/
|
|
14
|
+
export function searchNames(prefix, year) {
|
|
15
|
+
if (!prefix || typeof prefix !== 'string') return [];
|
|
16
|
+
|
|
17
|
+
const key = normalize(prefix);
|
|
18
|
+
if (key.length === 0) return [];
|
|
19
|
+
|
|
20
|
+
const y = year ?? new Date().getFullYear();
|
|
21
|
+
const idx = getIndex(y);
|
|
22
|
+
|
|
23
|
+
/** @type {Map<string, SearchResult>} */
|
|
24
|
+
const resultMap = new Map();
|
|
25
|
+
|
|
26
|
+
for (const [nameKey, entries] of idx.byName) {
|
|
27
|
+
if (!nameKey.startsWith(key)) continue;
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const dateStr = `${String(entry.month).padStart(2, '0')}-${String(entry.day).padStart(2, '0')}`;
|
|
31
|
+
const resultKey = `${entry.name}|${dateStr}`;
|
|
32
|
+
|
|
33
|
+
if (!resultMap.has(resultKey)) {
|
|
34
|
+
resultMap.set(resultKey, {
|
|
35
|
+
name: entry.name,
|
|
36
|
+
date: dateStr,
|
|
37
|
+
holiday: entry.holiday,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [...resultMap.values()];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all names celebrating a specific holiday.
|
|
48
|
+
* Case-insensitive partial match.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} holidayName - Holiday name to search for (Cyrillic or Latin)
|
|
51
|
+
* @param {number} [year] - Year for moveable dates (default: current year)
|
|
52
|
+
* @returns {string[]} Array of all celebrating names (primary + variants)
|
|
53
|
+
*/
|
|
54
|
+
export function getNamesByHoliday(holidayName, year) {
|
|
55
|
+
if (!holidayName || typeof holidayName !== 'string') return [];
|
|
56
|
+
|
|
57
|
+
const key = normalize(holidayName);
|
|
58
|
+
if (key.length === 0) return [];
|
|
59
|
+
|
|
60
|
+
const y = year ?? new Date().getFullYear();
|
|
61
|
+
const idx = getIndex(y);
|
|
62
|
+
|
|
63
|
+
const names = [];
|
|
64
|
+
|
|
65
|
+
for (const [holidayKey, entries] of idx.byHoliday) {
|
|
66
|
+
if (!holidayKey.includes(key)) continue;
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
names.push(entry.name);
|
|
70
|
+
for (const variant of entry.variants) {
|
|
71
|
+
names.push(variant);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Deduplicate while preserving order
|
|
77
|
+
return [...new Set(names)];
|
|
78
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cyrToLat, latToCyr, latKeys } from "./data/transliteration-map.js";
|
|
2
|
+
|
|
3
|
+
const CYR_REGEX = /[\u0400-\u04FF]/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects if text contains Cyrillic characters.
|
|
7
|
+
*/
|
|
8
|
+
function isCyrillic(text) {
|
|
9
|
+
return CYR_REGEX.test(text);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Transliterates Cyrillic text to Latin.
|
|
14
|
+
*/
|
|
15
|
+
function cyrillicToLatin(text) {
|
|
16
|
+
let result = "";
|
|
17
|
+
for (const char of text) {
|
|
18
|
+
result += cyrToLat[char] ?? char;
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Transliterates Latin text to Cyrillic.
|
|
25
|
+
*/
|
|
26
|
+
function latinToCyrillic(text) {
|
|
27
|
+
let result = "";
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < text.length) {
|
|
30
|
+
let matched = false;
|
|
31
|
+
for (const key of latKeys) {
|
|
32
|
+
if (text.startsWith(key, i)) {
|
|
33
|
+
result += latToCyr[key];
|
|
34
|
+
i += key.length;
|
|
35
|
+
matched = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!matched) {
|
|
40
|
+
result += text[i];
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Auto-detect direction and transliterate.
|
|
49
|
+
* Cyrillic input → Latin output, Latin input → Cyrillic output.
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function transliterate(text) {
|
|
54
|
+
if (typeof text !== "string" || text.length === 0) return "";
|
|
55
|
+
return isCyrillic(text) ? cyrillicToLatin(text) : latinToCyrillic(text);
|
|
56
|
+
}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} NameDayEntry
|
|
3
|
+
* @property {string} name - Primary name (canonical form, Cyrillic)
|
|
4
|
+
* @property {string[]} variants - Variant forms, diminutives, female forms
|
|
5
|
+
* @property {string[]} latin - Latin transliterations of name + variants
|
|
6
|
+
* @property {number} month - Month (1-12), 0 for moveable dates
|
|
7
|
+
* @property {number} day - Day (1-31), 0 for moveable dates
|
|
8
|
+
* @property {string} holiday - Holiday name in Bulgarian
|
|
9
|
+
* @property {string} holidayLatin - Holiday name in Latin transliteration
|
|
10
|
+
* @property {'orthodox'|'folk'|'both'} tradition - Tradition source
|
|
11
|
+
* @property {string|null} moveable - null for fixed dates, offset string for moveable (e.g. "easter", "easter-7", "easter+39")
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} NameDayResult
|
|
16
|
+
* @property {string} name - Primary name (Cyrillic)
|
|
17
|
+
* @property {number} month - Month (1-12)
|
|
18
|
+
* @property {number} day - Day (1-31)
|
|
19
|
+
* @property {string} holiday - Holiday name in Bulgarian
|
|
20
|
+
* @property {string} holidayLatin - Holiday name in Latin transliteration
|
|
21
|
+
* @property {'orthodox'|'folk'|'both'} tradition - Tradition source
|
|
22
|
+
* @property {string[]} variants - All variant forms
|
|
23
|
+
* @property {boolean} isMoveable - Whether this is a moveable feast date
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} MoveableHoliday
|
|
28
|
+
* @property {string} id - Unique identifier (e.g. "easter", "tsvetnitsa")
|
|
29
|
+
* @property {string} holiday - Holiday name in Bulgarian
|
|
30
|
+
* @property {string} holidayLatin - Holiday name in Latin transliteration
|
|
31
|
+
* @property {number} offsetFromEaster - Days offset from Easter (negative = before, positive = after, 0 = Easter itself)
|
|
32
|
+
* @property {'orthodox'|'folk'|'both'} tradition - Tradition source
|
|
33
|
+
* @property {MoveableNameEntry[]} entries - Name entries for this holiday
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} MoveableNameEntry
|
|
38
|
+
* @property {string} name - Primary name (Cyrillic)
|
|
39
|
+
* @property {string[]} variants - Variant forms
|
|
40
|
+
* @property {string[]} latin - Latin transliterations
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} UpcomingNameDay
|
|
45
|
+
* @property {number} month - Month (1-12)
|
|
46
|
+
* @property {number} day - Day (1-31)
|
|
47
|
+
* @property {string} holiday - Holiday name in Bulgarian
|
|
48
|
+
* @property {string[]} names - All celebrating names (primary + variants)
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} SearchResult
|
|
53
|
+
* @property {string} name - Name found (Cyrillic)
|
|
54
|
+
* @property {string} date - Date string "MM-DD"
|
|
55
|
+
* @property {string} holiday - Holiday name
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
export {};
|
package/src/upcoming.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').UpcomingNameDay} UpcomingNameDay */
|
|
2
|
+
|
|
3
|
+
import { getIndex } from './data/nameDays.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get upcoming name days within a given number of days.
|
|
7
|
+
* Handles year boundaries (December → January).
|
|
8
|
+
*
|
|
9
|
+
* @param {number} days - Number of days to look ahead
|
|
10
|
+
* @param {Date} [startDate] - Start date (default: today)
|
|
11
|
+
* @returns {UpcomingNameDay[]} Sorted by date
|
|
12
|
+
*/
|
|
13
|
+
export function getUpcomingNameDays(days, startDate) {
|
|
14
|
+
if (!days || typeof days !== 'number' || days < 1) return [];
|
|
15
|
+
|
|
16
|
+
const start = startDate instanceof Date && !Number.isNaN(startDate.getTime())
|
|
17
|
+
? startDate
|
|
18
|
+
: new Date();
|
|
19
|
+
|
|
20
|
+
/** @type {UpcomingNameDay[]} */
|
|
21
|
+
const results = [];
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < days; i++) {
|
|
24
|
+
const current = new Date(start);
|
|
25
|
+
current.setDate(current.getDate() + i);
|
|
26
|
+
|
|
27
|
+
const month = current.getMonth() + 1;
|
|
28
|
+
const day = current.getDate();
|
|
29
|
+
const year = current.getFullYear();
|
|
30
|
+
const dateKey = `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
31
|
+
|
|
32
|
+
const idx = getIndex(year);
|
|
33
|
+
const entries = idx.byDate.get(dateKey);
|
|
34
|
+
|
|
35
|
+
if (!entries || entries.length === 0) continue;
|
|
36
|
+
|
|
37
|
+
// Collect all names for this date, grouped by holiday
|
|
38
|
+
/** @type {Map<string, {holiday: string, names: string[]}>} */
|
|
39
|
+
const byHoliday = new Map();
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const key = entry.holiday;
|
|
43
|
+
if (!byHoliday.has(key)) {
|
|
44
|
+
byHoliday.set(key, { holiday: entry.holiday, names: [] });
|
|
45
|
+
}
|
|
46
|
+
const group = byHoliday.get(key);
|
|
47
|
+
group.names.push(entry.name);
|
|
48
|
+
for (const variant of entry.variants) {
|
|
49
|
+
group.names.push(variant);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const group of byHoliday.values()) {
|
|
54
|
+
results.push({
|
|
55
|
+
month,
|
|
56
|
+
day,
|
|
57
|
+
holiday: group.holiday,
|
|
58
|
+
names: [...new Set(group.names)],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return results;
|
|
64
|
+
}
|