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.
@@ -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 {};
@@ -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
+ }