yokatlas-api-wrapper 1.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/dist/index.js ADDED
@@ -0,0 +1,827 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BIRIM_TURU: () => BIRIM_TURU,
24
+ BIRIM_TURU_ETIKETLERI: () => BIRIM_TURU_ETIKETLERI,
25
+ BURS_ORANI: () => BURS_ORANI,
26
+ PUAN_TURU_ETIKETLERI: () => PUAN_TURU_ETIKETLERI,
27
+ UNIVERSITE_TURU_ETIKETLERI: () => UNIVERSITE_TURU_ETIKETLERI,
28
+ YokAtlas: () => YokAtlas,
29
+ YokAtlasAPIError: () => YokAtlasAPIError,
30
+ YokAtlasError: () => YokAtlasError,
31
+ YokAtlasLookupError: () => YokAtlasLookupError,
32
+ YokAtlasNotFoundError: () => YokAtlasNotFoundError,
33
+ YokAtlasRateLimitError: () => YokAtlasRateLimitError,
34
+ YokAtlasValidationError: () => YokAtlasValidationError,
35
+ default: () => index_default,
36
+ getBirimTuruLabel: () => getBirimTuruLabel,
37
+ getPuanTuruLabel: () => getPuanTuruLabel,
38
+ getUniversiteTuruLabel: () => getUniversiteTuruLabel
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/errors.ts
43
+ var YokAtlasError = class extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = "YokAtlasError";
47
+ Object.setPrototypeOf(this, new.target.prototype);
48
+ }
49
+ };
50
+ var YokAtlasValidationError = class extends YokAtlasError {
51
+ constructor(message) {
52
+ super(message);
53
+ this.name = "YokAtlasValidationError";
54
+ Object.setPrototypeOf(this, new.target.prototype);
55
+ }
56
+ };
57
+ var YokAtlasAPIError = class extends YokAtlasError {
58
+ status;
59
+ body;
60
+ cause;
61
+ constructor(message, options) {
62
+ super(message);
63
+ this.name = "YokAtlasAPIError";
64
+ this.status = options?.status;
65
+ this.body = options?.body;
66
+ this.cause = options?.cause;
67
+ Object.setPrototypeOf(this, new.target.prototype);
68
+ }
69
+ };
70
+ var YokAtlasNotFoundError = class extends YokAtlasAPIError {
71
+ constructor(message, options) {
72
+ super(message, options);
73
+ this.name = "YokAtlasNotFoundError";
74
+ Object.setPrototypeOf(this, new.target.prototype);
75
+ }
76
+ };
77
+ var YokAtlasRateLimitError = class extends YokAtlasAPIError {
78
+ constructor(message, options) {
79
+ super(message, options);
80
+ this.name = "YokAtlasRateLimitError";
81
+ Object.setPrototypeOf(this, new.target.prototype);
82
+ }
83
+ };
84
+ var YokAtlasLookupError = class extends YokAtlasError {
85
+ query;
86
+ kind;
87
+ suggestions;
88
+ constructor(query, options) {
89
+ const base = query ? `'${query}' i\xE7in bir ${options.kind} bulunamad\u0131.` : `Bo\u015F bir ${options.kind} ad\u0131 \xE7\xF6z\xFCmlenemez.`;
90
+ const message = options.suggestions && options.suggestions.length > 0 ? `${base} \u015Eunu mu demek istediniz: ${options.suggestions.join(", ")}?` : base;
91
+ super(message);
92
+ this.name = "YokAtlasLookupError";
93
+ this.query = query;
94
+ this.kind = options.kind;
95
+ this.suggestions = options.suggestions ?? [];
96
+ Object.setPrototypeOf(this, new.target.prototype);
97
+ }
98
+ };
99
+
100
+ // src/lookup.ts
101
+ var TR_MAP = {
102
+ "\u0130": "I",
103
+ "\u0131": "i",
104
+ "\u011E": "G",
105
+ "\u011F": "g",
106
+ "\u015E": "S",
107
+ "\u015F": "s",
108
+ "\xC7": "C",
109
+ "\xE7": "c",
110
+ "\xD6": "O",
111
+ "\xF6": "o",
112
+ "\xDC": "U",
113
+ "\xFC": "u"
114
+ };
115
+ function normalizeTurkish(text) {
116
+ const ascii = text.replace(/[İıĞğŞşÇçÖöÜü]/g, (ch) => TR_MAP[ch] ?? ch);
117
+ return ascii.toLowerCase().trim().replace(/\s+/g, " ");
118
+ }
119
+ function levenshteinDistance(a, b) {
120
+ const m = a.length;
121
+ const n = b.length;
122
+ if (m === 0) return n;
123
+ if (n === 0) return m;
124
+ let prev = new Array(n + 1);
125
+ let curr = new Array(n + 1);
126
+ for (let j = 0; j <= n; j++) prev[j] = j;
127
+ for (let i = 1; i <= m; i++) {
128
+ curr[0] = i;
129
+ for (let j = 1; j <= n; j++) {
130
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
131
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
132
+ }
133
+ [prev, curr] = [curr, prev];
134
+ }
135
+ return prev[n];
136
+ }
137
+ function similarityRatio(a, b) {
138
+ const maxLen = Math.max(a.length, b.length);
139
+ if (maxLen === 0) return 1;
140
+ return 1 - levenshteinDistance(a, b) / maxLen;
141
+ }
142
+ function resolveByName(query, items, nameOf, kind, cutoff = 0.6) {
143
+ if (!query || !query.trim()) {
144
+ throw new YokAtlasLookupError(query, { kind });
145
+ }
146
+ const key = normalizeTurkish(query);
147
+ for (const item of items) {
148
+ if (normalizeTurkish(nameOf(item)) === key) return item;
149
+ }
150
+ for (const item of items) {
151
+ const norm = normalizeTurkish(nameOf(item));
152
+ if (norm.includes(key) || key.includes(norm)) return item;
153
+ }
154
+ let best = null;
155
+ let bestScore = -1;
156
+ const scored = [];
157
+ for (const item of items) {
158
+ const score = similarityRatio(key, normalizeTurkish(nameOf(item)));
159
+ scored.push({ item, score });
160
+ if (score > bestScore) {
161
+ bestScore = score;
162
+ best = item;
163
+ }
164
+ }
165
+ if (best && bestScore >= cutoff) return best;
166
+ const suggestions = scored.sort((x, y) => y.score - x.score).filter((s) => s.score >= 0.4).slice(0, 3).map((s) => nameOf(s.item));
167
+ throw new YokAtlasLookupError(query, { kind, suggestions });
168
+ }
169
+
170
+ // src/yokatlas.ts
171
+ var SEARCH_PATH = "/api/tercih-kilavuz/search";
172
+ var UNIVERSITIES_PATH = "/api/tercih-kilavuz/universiteler";
173
+ var PROGRAMS_PATH = "/api/tercih-kilavuz/universite-programlar";
174
+ var CITIES_PATH = "/api/tercih-kilavuz/universite-iller";
175
+ var NETLER_SEARCH_PATH = "/api/netler/search";
176
+ var YEARLY_OFFSET_FIELDS = {
177
+ kontenjan: "kontenjan",
178
+ yerlesen: "gkY",
179
+ kontenjanObs: "kontenjanObs",
180
+ kontenjanY34: "kontenjanY34",
181
+ prof: "prof",
182
+ doc: "doc",
183
+ dou: "dou",
184
+ ogrGor: "ogrGor",
185
+ arGor: "arGor",
186
+ kpss1: "kpss1",
187
+ kpss2: "kpss2",
188
+ minPuan: "minPuan",
189
+ basariSirasi: "basariSirasi"
190
+ };
191
+ var YokAtlas = class {
192
+ static baseUrl = "https://yokatlas.yok.gov.tr";
193
+ static timeoutMs = 3e4;
194
+ static userAgent = "yokatlas-api-wrapper/1.0 (+https://www.npmjs.com/package/yokatlas-api-wrapper)";
195
+ static maxRetries = 2;
196
+ static lookupCacheTtlMs = 36e5;
197
+ static lookupCache = null;
198
+ constructor() {
199
+ }
200
+ // ---------------------------------------------------------------------
201
+ // Yapılandırma
202
+ // ---------------------------------------------------------------------
203
+ /** İstemcinin temel URL'sini, zaman aşımını, User-Agent'ını, retry ve önbellek ayarlarını değiştirir. */
204
+ static configure(config) {
205
+ if (config.baseUrl !== void 0) this.baseUrl = config.baseUrl.replace(/\/+$/, "");
206
+ if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
207
+ if (config.userAgent !== void 0) this.userAgent = config.userAgent;
208
+ if (config.maxRetries !== void 0) this.maxRetries = config.maxRetries;
209
+ if (config.lookupCacheTtlMs !== void 0) this.lookupCacheTtlMs = config.lookupCacheTtlMs;
210
+ }
211
+ // ---------------------------------------------------------------------
212
+ // Temel arama
213
+ // ---------------------------------------------------------------------
214
+ /** YÖK Atlas tercih kılavuzunda program arar. */
215
+ static async search(filters = {}, options = {}) {
216
+ const { page = 0, size = 20, sortBy = "basariSirasi", direction = "ASC", smartSearch = true, signal } = options;
217
+ let resolved = filters;
218
+ if (smartSearch && (filters.universite != null || filters.program != null || filters.il != null)) {
219
+ await this.ensureLookups();
220
+ resolved = this.resolveSmartFilters(filters);
221
+ }
222
+ const body = {
223
+ filters: this.filtersToPayload(resolved),
224
+ page,
225
+ size,
226
+ sortBy,
227
+ direction: direction.toUpperCase()
228
+ };
229
+ const raw = await this.postJson(SEARCH_PATH, body, signal);
230
+ return this.toSearchPage(raw, (row) => this.buildProgram(row));
231
+ }
232
+ /** Net Sihirbazı'nı sorgular: son yerleşen kişinin TYT/AYT/YDT netleri. */
233
+ static async searchNetler(filters = {}, options = {}) {
234
+ const { page = 0, size = 20, smartSearch = true, signal } = options;
235
+ let resolved = filters;
236
+ if (smartSearch && (filters.universite != null || filters.program != null)) {
237
+ await this.ensureLookups();
238
+ resolved = this.resolveNetSmartFilters(filters);
239
+ }
240
+ const body = { filters: this.netFiltersToPayload(resolved), page, size };
241
+ const raw = await this.postJson(NETLER_SEARCH_PATH, body, signal);
242
+ return this.toSearchPage(raw, (row) => this.buildNet(row));
243
+ }
244
+ /** Tek bir programı ÖSYM kılavuz kodundan getirir; bulunamazsa `null` döner. */
245
+ static async getProgram(kilavuzKodu, signal) {
246
+ const code = Number(kilavuzKodu);
247
+ if (!Number.isFinite(code)) {
248
+ throw new YokAtlasValidationError(`kilavuzKodu bir say\u0131 olmal\u0131 (al\u0131nan: ${String(kilavuzKodu)})`);
249
+ }
250
+ const page = await this.search({ kilavuzKodu: code }, { size: 1, smartSearch: false, signal });
251
+ return page.content[0] ?? null;
252
+ }
253
+ /**
254
+ * Birden çok programı kılavuz koduyla, sınırlı eşzamanlılıkla getirir.
255
+ * Her kod bağımsız çözülür — biri başarısız olursa diğerlerini etkilemez.
256
+ */
257
+ static async getPrograms(kilavuzKodlari, options = {}) {
258
+ const { concurrency = 4, signal } = options;
259
+ const results = new Array(
260
+ kilavuzKodlari.length
261
+ );
262
+ let index = 0;
263
+ const worker = async () => {
264
+ while (index < kilavuzKodlari.length) {
265
+ const i = index++;
266
+ const kod = kilavuzKodlari[i];
267
+ try {
268
+ const program = await this.getProgram(kod, signal);
269
+ results[i] = { kilavuzKodu: kod, status: "fulfilled", program };
270
+ } catch (reason) {
271
+ results[i] = { kilavuzKodu: kod, status: "rejected", reason };
272
+ }
273
+ }
274
+ };
275
+ const workers = Array.from({ length: Math.max(1, Math.min(concurrency, kilavuzKodlari.length || 1)) }, () => worker());
276
+ await Promise.all(workers);
277
+ return results;
278
+ }
279
+ /**
280
+ * `search()`'ü sayfa sayfa dolaşıp tüm sonuçları düz bir diziye toplar.
281
+ * `maxPages` güvenlik sınırına takılırsa erken durur.
282
+ */
283
+ static async searchAllPages(filters = {}, options = {}) {
284
+ const { pageSize = 100, maxPages = 50, sortBy, direction, smartSearch = true, signal } = options;
285
+ const all = [];
286
+ let resolvedFilters = filters;
287
+ if (smartSearch && (filters.universite != null || filters.program != null || filters.il != null)) {
288
+ await this.ensureLookups(signal);
289
+ resolvedFilters = this.resolveSmartFilters(filters);
290
+ }
291
+ for (let page = 0; page < maxPages; page++) {
292
+ const result = await this.search(resolvedFilters, {
293
+ page,
294
+ size: pageSize,
295
+ sortBy,
296
+ direction,
297
+ smartSearch: false,
298
+ signal
299
+ });
300
+ all.push(...result.content);
301
+ if (result.last || result.content.length === 0) break;
302
+ }
303
+ return all;
304
+ }
305
+ // ---------------------------------------------------------------------
306
+ // Lookup tabloları
307
+ // ---------------------------------------------------------------------
308
+ /** Tüm üniversiteleri (ID + ad) döner. */
309
+ static async listUniversities(signal) {
310
+ await this.ensureLookups(signal);
311
+ return [...this.lookupCache.universities];
312
+ }
313
+ /** Tüm program gruplarını (ID + ad + puan türü) döner. */
314
+ static async listProgramGroups(signal) {
315
+ await this.ensureLookups(signal);
316
+ return [...this.lookupCache.programGroups];
317
+ }
318
+ /** Tüm illeri (kod + ad) döner. */
319
+ static async listCities(signal) {
320
+ await this.ensureLookups(signal);
321
+ return [...this.lookupCache.cities];
322
+ }
323
+ /** Lookup önbelleğini zorla yeniler. */
324
+ static async refreshLookups(signal) {
325
+ this.lookupCache = null;
326
+ await this.fetchLookups(signal);
327
+ }
328
+ /** Lookup önbelleğini boşaltır (bir sonraki çağrıda yeniden çekilir). */
329
+ static clearCache() {
330
+ this.lookupCache = null;
331
+ }
332
+ /** Lookup önbelleğinin durumunu (dolu mu, ne zaman çekildi, kaç kayıt var) döner. */
333
+ static getCacheStatus() {
334
+ const cache = this.lookupCache;
335
+ return {
336
+ cached: cache !== null,
337
+ fetchedAt: cache?.fetchedAt ?? null,
338
+ ageMs: cache ? Date.now() - cache.fetchedAt : null,
339
+ ttlMs: this.lookupCacheTtlMs,
340
+ universiteSayisi: cache?.universities.length ?? 0,
341
+ programGrubuSayisi: cache?.programGroups.length ?? 0,
342
+ ilSayisi: cache?.cities.length ?? 0
343
+ };
344
+ }
345
+ /** Serbest yazılmış bir üniversite adını fuzzy eşleştirerek {@link University} kaydına çözer. */
346
+ static async findUniversity(name, signal) {
347
+ await this.ensureLookups(signal);
348
+ return resolveByName(name, this.lookupCache.universities, (u) => u.universiteAdi, "\xFCniversite");
349
+ }
350
+ /** Serbest yazılmış bir program adını fuzzy eşleştirerek {@link ProgramGroup} kaydına çözer. */
351
+ static async findProgramGroup(name, signal) {
352
+ await this.ensureLookups(signal);
353
+ return resolveByName(name, this.lookupCache.programGroups, (p) => p.birimGrupAdi, "program");
354
+ }
355
+ /** Serbest yazılmış bir il adını fuzzy eşleştirerek {@link City} kaydına çözer. */
356
+ static async findCity(name, signal) {
357
+ await this.ensureLookups(signal);
358
+ return resolveByName(name, this.lookupCache.cities, (c) => c.ilAdi, "il");
359
+ }
360
+ // ---------------------------------------------------------------------
361
+ // Kısayol aramalar
362
+ // ---------------------------------------------------------------------
363
+ /** Belirli bir üniversitenin (serbest yazım) tüm programlarını arar. */
364
+ static async searchByUniversity(universiteAdi, filters = {}, options = {}) {
365
+ return this.search({ ...filters, universite: universiteAdi }, options);
366
+ }
367
+ /** Belirli bir program grubunu (serbest yazım) tüm üniversitelerde arar. */
368
+ static async searchByProgram(programAdi, filters = {}, options = {}) {
369
+ return this.search({ ...filters, program: programAdi }, options);
370
+ }
371
+ /** Belirli bir ildeki (serbest yazım) tüm programları arar. */
372
+ static async searchByCity(ilAdi, filters = {}, options = {}) {
373
+ return this.search({ ...filters, il: ilAdi }, options);
374
+ }
375
+ /** Sadece lisans (4 yıllık) programlarını arar. */
376
+ static async searchLisans(filters = {}, options = {}) {
377
+ return this.search({ ...filters, birimTuruId: 46 }, options);
378
+ }
379
+ /** Sadece ön lisans (2 yıllık) programlarını arar. */
380
+ static async searchOnlisans(filters = {}, options = {}) {
381
+ return this.search({ ...filters, birimTuruId: 47 }, options);
382
+ }
383
+ /** Sadece ücretsiz/tam burslu programları arar. */
384
+ static async searchBurslu(filters = {}, options = {}) {
385
+ return this.search({ ...filters, bursOraniId: 0 }, options);
386
+ }
387
+ /** Belirli bir başarı sırası aralığındaki programları arar (`minBasariSirasi`/`maxBasariSirasi` kısayolu). */
388
+ static async searchByScoreRange(range, filters = {}, options = {}) {
389
+ return this.search({ ...filters, minBasariSirasi: range.min ?? null, maxBasariSirasi: range.max ?? null }, options);
390
+ }
391
+ // ---------------------------------------------------------------------
392
+ // Türetilmiş / çevrimdışı analiz yardımcıları (ekstra ağ isteği yapmaz)
393
+ // ---------------------------------------------------------------------
394
+ /**
395
+ * Kullanıcının kendi başarı sırasını, bir programın güncel yıl kesme
396
+ * sırasıyla karşılaştırarak kaba bir yerleşme tahmini üretir. Sıra ne
397
+ * kadar küçükse o kadar iyi bir konumdur (1. sıra en iyisidir).
398
+ */
399
+ static estimateAdmission(program, basariSirasi) {
400
+ const cutoff = program.current.basariSirasi;
401
+ if (cutoff === null) {
402
+ return {
403
+ program,
404
+ basariSirasi,
405
+ cutoffBasariSirasi: null,
406
+ verdict: "belirsiz",
407
+ margin: null,
408
+ message: "Bu program i\xE7in g\xFCncel y\u0131l ba\u015Far\u0131 s\u0131ras\u0131 verisi yok; tahmin yap\u0131lam\u0131yor."
409
+ };
410
+ }
411
+ const margin = basariSirasi - cutoff;
412
+ const relativeMargin = margin / cutoff;
413
+ let verdict;
414
+ let message;
415
+ if (relativeMargin <= -0.1) {
416
+ verdict = "kesine yak\u0131n";
417
+ message = `S\u0131ran\u0131z (${basariSirasi.toLocaleString("tr-TR")}) ge\xE7en y\u0131lki kesme s\u0131ras\u0131ndan (${cutoff.toLocaleString("tr-TR")}) belirgin \u015Fekilde iyi; yerle\u015Fme olas\u0131l\u0131\u011F\u0131n\u0131z y\xFCksek.`;
418
+ } else if (relativeMargin <= 0) {
419
+ verdict = "olas\u0131";
420
+ message = `S\u0131ran\u0131z (${basariSirasi.toLocaleString("tr-TR")}) ge\xE7en y\u0131lki kesme s\u0131ras\u0131ndan (${cutoff.toLocaleString("tr-TR")}) iyi; yerle\u015Fmeniz olas\u0131 ama kesin de\u011Fil.`;
421
+ } else if (relativeMargin <= 0.1) {
422
+ verdict = "s\u0131n\u0131rda";
423
+ message = `S\u0131ran\u0131z (${basariSirasi.toLocaleString("tr-TR")}) ge\xE7en y\u0131lki kesme s\u0131ras\u0131na (${cutoff.toLocaleString("tr-TR")}) yak\u0131n; s\u0131n\u0131rda bir durum.`;
424
+ } else {
425
+ verdict = "zay\u0131f";
426
+ message = `S\u0131ran\u0131z (${basariSirasi.toLocaleString("tr-TR")}) ge\xE7en y\u0131lki kesme s\u0131ras\u0131ndan (${cutoff.toLocaleString("tr-TR")}) belirgin \u015Fekilde geride; yerle\u015Fme olas\u0131l\u0131\u011F\u0131n\u0131z d\xFC\u015F\xFCk.`;
427
+ }
428
+ return { program, basariSirasi, cutoffBasariSirasi: cutoff, verdict, margin, message };
429
+ }
430
+ /** İki programı güncel yıl rekabet düzeyi (başarı sırası) ve kontenjan açısından karşılaştırır. */
431
+ static compare(a, b) {
432
+ const aScore = a.current.basariSirasi;
433
+ const bScore = b.current.basariSirasi;
434
+ let moreCompetitive = "belirsiz";
435
+ if (aScore !== null && bScore !== null) {
436
+ if (aScore < bScore) moreCompetitive = "a";
437
+ else if (bScore < aScore) moreCompetitive = "b";
438
+ else moreCompetitive = "e\u015Fit";
439
+ }
440
+ const scoreDiff = aScore !== null && bScore !== null ? aScore - bScore : null;
441
+ const aQuota = a.current.kontenjan;
442
+ const bQuota = b.current.kontenjan;
443
+ const quotaDiff = aQuota !== null && bQuota !== null ? aQuota - bQuota : null;
444
+ return { a, b, moreCompetitive, scoreDiff, quotaDiff };
445
+ }
446
+ /**
447
+ * Bir programın `history` + `current` verisindeki başarı sırası dizisine
448
+ * bakarak eğilimini ("yükseliyor"/"düşüyor"/"sabit") kestirir. Başarı
449
+ * sırasının küçülmesi programın *daha* rekabetçi hale geldiği anlamına gelir.
450
+ */
451
+ static getTrend(program) {
452
+ const series = [...program.history].slice().reverse().concat(program.current).map((s) => ({ year: s.year, basariSirasi: s.basariSirasi }));
453
+ const known = series.filter((s) => s.basariSirasi !== null);
454
+ if (known.length < 2) {
455
+ return { program, direction: "belirsiz", series, message: "E\u011Filim hesaplamak i\xE7in yeterli y\u0131ll\u0131k veri yok." };
456
+ }
457
+ const first = known[0].basariSirasi;
458
+ const last = known[known.length - 1].basariSirasi;
459
+ const changeRatio = (last - first) / first;
460
+ let direction;
461
+ let message;
462
+ if (changeRatio <= -0.05) {
463
+ direction = "y\xFCkseliyor";
464
+ message = `Ba\u015Far\u0131 s\u0131ras\u0131 ${known[0].year}'den ${known[known.length - 1].year}'e k\xFC\xE7\xFClm\xFC\u015F (${first.toLocaleString("tr-TR")} \u2192 ${last.toLocaleString("tr-TR")}); program giderek daha rekabet\xE7i hale geliyor.`;
465
+ } else if (changeRatio >= 0.05) {
466
+ direction = "d\xFC\u015F\xFCyor";
467
+ message = `Ba\u015Far\u0131 s\u0131ras\u0131 ${known[0].year}'den ${known[known.length - 1].year}'e b\xFCy\xFCm\xFC\u015F (${first.toLocaleString("tr-TR")} \u2192 ${last.toLocaleString("tr-TR")}); program\u0131n rekabet d\xFCzeyi azal\u0131yor.`;
468
+ } else {
469
+ direction = "sabit";
470
+ message = `Ba\u015Far\u0131 s\u0131ras\u0131 ${known[0].year}'den ${known[known.length - 1].year}'e b\xFCy\xFCk \xF6l\xE7\xFCde stabil kalm\u0131\u015F (${first.toLocaleString("tr-TR")} \u2192 ${last.toLocaleString("tr-TR")}).`;
471
+ }
472
+ return { program, direction, series, message };
473
+ }
474
+ /** Bir program dizisini, verilen anahtar fonksiyonuna göre gruplar. */
475
+ static groupBy(programs, keyFn) {
476
+ const result = {};
477
+ for (const program of programs) {
478
+ const key = keyFn(program);
479
+ if (!result[key]) result[key] = [];
480
+ result[key].push(program);
481
+ }
482
+ return result;
483
+ }
484
+ /** Bir program dizisini güncel yıl başarı sırasına göre sıralar (varsayılan: küçükten büyüğe / en iyi önce). */
485
+ static sortByScore(programs, direction = "asc") {
486
+ const withScore = programs.filter((p) => p.current.basariSirasi !== null);
487
+ const withoutScore = programs.filter((p) => p.current.basariSirasi === null);
488
+ withScore.sort((a, b) => {
489
+ const diff = a.current.basariSirasi - b.current.basariSirasi;
490
+ return direction === "asc" ? diff : -diff;
491
+ });
492
+ return [...withScore, ...withoutScore];
493
+ }
494
+ /** Bir programı tek satırlık okunur bir özet metnine çevirir (log/konsol için). */
495
+ static formatSummary(program) {
496
+ const puan = program.current.minPuan !== null ? program.current.minPuan.toLocaleString("tr-TR") : "\u2014";
497
+ const sira = program.current.basariSirasi !== null ? program.current.basariSirasi.toLocaleString("tr-TR") : "\u2014";
498
+ return `${program.universiteAdi} \u2014 ${program.birimAdi} (${program.puanTuru}, ${program.current.year}) | Puan: ${puan} \xB7 S\u0131ra: ${sira}`;
499
+ }
500
+ // ---------------------------------------------------------------------
501
+ // İç mekanizma — lookup önbelleği
502
+ // ---------------------------------------------------------------------
503
+ static async ensureLookups(signal) {
504
+ const cache = this.lookupCache;
505
+ const fresh = cache !== null && (this.lookupCacheTtlMs <= 0 || Date.now() - cache.fetchedAt < this.lookupCacheTtlMs);
506
+ if (fresh) return;
507
+ await this.fetchLookups(signal);
508
+ }
509
+ static async fetchLookups(signal) {
510
+ const [universities, programGroups, cities] = await Promise.all([
511
+ this.getJson(UNIVERSITIES_PATH, signal),
512
+ this.getJson(PROGRAMS_PATH, signal),
513
+ this.getJson(CITIES_PATH, signal)
514
+ ]);
515
+ this.lookupCache = { universities, programGroups, cities, fetchedAt: Date.now() };
516
+ }
517
+ static resolveSmartFilters(filters) {
518
+ const cache = this.lookupCache;
519
+ const resolved = { ...filters };
520
+ if (filters.universite != null) {
521
+ const names = Array.isArray(filters.universite) ? filters.universite : [filters.universite];
522
+ resolved.universiteId = names.map((n) => resolveByName(n, cache.universities, (u) => u.universiteAdi, "\xFCniversite").universiteId);
523
+ resolved.universite = null;
524
+ }
525
+ if (filters.program != null) {
526
+ const names = Array.isArray(filters.program) ? filters.program : [filters.program];
527
+ resolved.birimGrupId = names.map((n) => resolveByName(n, cache.programGroups, (p) => p.birimGrupAdi, "program").birimGrupId);
528
+ resolved.program = null;
529
+ }
530
+ if (filters.il != null) {
531
+ const names = Array.isArray(filters.il) ? filters.il : [filters.il];
532
+ resolved.ilKodu = names.map((n) => resolveByName(n, cache.cities, (c) => c.ilAdi, "il").ilKodu);
533
+ resolved.il = null;
534
+ }
535
+ return resolved;
536
+ }
537
+ static resolveNetSmartFilters(filters) {
538
+ const cache = this.lookupCache;
539
+ const resolved = { ...filters };
540
+ if (filters.universite != null) {
541
+ resolved.universiteId = resolveByName(filters.universite, cache.universities, (u) => u.universiteAdi, "\xFCniversite").universiteId;
542
+ resolved.universite = null;
543
+ }
544
+ if (filters.program != null) {
545
+ const resolvedProgram = resolveByName(filters.program, cache.programGroups, (p) => p.birimGrupAdi, "program");
546
+ resolved.birimGrupId = resolvedProgram.birimGrupId;
547
+ resolved.program = null;
548
+ if (filters.puanTuru == null) {
549
+ resolved.puanTuru = resolvedProgram.puanTuru;
550
+ }
551
+ }
552
+ return resolved;
553
+ }
554
+ // ---------------------------------------------------------------------
555
+ // İç mekanizma — payload/response dönüşümleri
556
+ // ---------------------------------------------------------------------
557
+ static normalizePuanTuru(value) {
558
+ if (!value) return null;
559
+ const upper = value.toUpperCase();
560
+ if (upper === "SOZ") return "S\xD6Z";
561
+ if (upper === "DIL") return "D\u0130L";
562
+ return upper;
563
+ }
564
+ static filtersToPayload(f) {
565
+ return {
566
+ puanTuru: this.normalizePuanTuru(f.puanTuru),
567
+ universiteId: f.universiteId ?? [],
568
+ birimGrupId: f.birimGrupId ?? [],
569
+ ilKodu: f.ilKodu ?? [],
570
+ birimTuruId: f.birimTuruId ?? null,
571
+ universiteTuru: f.universiteTuru ?? null,
572
+ bursOraniId: f.bursOraniId ?? null,
573
+ ogrenimTuruId: f.ogrenimTuruId ?? null,
574
+ kilavuzKodu: f.kilavuzKodu ?? null,
575
+ minBasariSirasi: f.minBasariSirasi ?? null,
576
+ maxBasariSirasi: f.maxBasariSirasi ?? null
577
+ };
578
+ }
579
+ static netFiltersToPayload(f) {
580
+ return {
581
+ puanTuru: this.normalizePuanTuru(f.puanTuru),
582
+ universiteId: f.universiteId ?? null,
583
+ birimGrupId: f.birimGrupId ?? null,
584
+ birimTuruId: f.birimTuruId ?? null,
585
+ universiteTuru: f.universiteTuru ?? null,
586
+ yil: f.yil != null ? String(f.yil) : null,
587
+ katsayi: f.katsayi ?? null
588
+ };
589
+ }
590
+ static toSearchPage(raw, mapRow) {
591
+ const content = Array.isArray(raw.content) ? raw.content : [];
592
+ return {
593
+ content: content.map(mapRow),
594
+ totalElements: Number(raw.totalElements ?? content.length),
595
+ totalPages: Number(raw.totalPages ?? 1),
596
+ size: Number(raw.size ?? content.length),
597
+ number: Number(raw.number ?? 0),
598
+ first: Boolean(raw.first ?? true),
599
+ last: Boolean(raw.last ?? true),
600
+ numberOfElements: Number(raw.numberOfElements ?? content.length),
601
+ empty: Boolean(raw.empty ?? content.length === 0),
602
+ yil: raw.yil != null ? Number(raw.yil) : null,
603
+ source: raw.source ?? null
604
+ };
605
+ }
606
+ static normalizeOnlisansSpelling(row) {
607
+ if (row.birimTuruAdi === "\xD6NLISANS") {
608
+ return { ...row, birimTuruAdi: "ONLISANS" };
609
+ }
610
+ return row;
611
+ }
612
+ static toNumber(value) {
613
+ if (value === null || value === void 0 || value === "") return null;
614
+ const n = Number(value);
615
+ return Number.isFinite(n) ? n : null;
616
+ }
617
+ static buildYearlyStats(row, suffix, year) {
618
+ const stats = { year };
619
+ for (const [field, apiBase] of Object.entries(YEARLY_OFFSET_FIELDS)) {
620
+ stats[field] = this.toNumber(row[`${apiBase}${suffix}`]);
621
+ }
622
+ return stats;
623
+ }
624
+ static buildProgram(rawRow) {
625
+ const row = this.normalizeOnlisansSpelling(rawRow);
626
+ const year = this.toNumber(row.yil) ?? 0;
627
+ const current = this.buildYearlyStats(row, "", year);
628
+ const history = [1, 2, 3].map((offset) => this.buildYearlyStats(row, String(offset), year - offset));
629
+ return {
630
+ osymKilavuzId: this.toNumber(row.osymKilavuzId),
631
+ sinav: row.sinav ?? null,
632
+ yil: year,
633
+ donem: row.donem ?? null,
634
+ tabloTuru: row.tabloTuru ?? null,
635
+ birimId: this.toNumber(row.birimId),
636
+ birimHiyerarsi: row.birimHiyerarsi ?? null,
637
+ kilavuzKodu: this.toNumber(row.kilavuzKodu),
638
+ universiteId: this.toNumber(row.universiteId),
639
+ universiteAdi: row.universiteAdi,
640
+ uniIlKodu: this.toNumber(row.uniIlKodu),
641
+ uniIlAdi: row.uniIlAdi ?? null,
642
+ uniIlceKodu: this.toNumber(row.uniIlceKodu),
643
+ uniIlceAdi: row.uniIlceAdi ?? null,
644
+ fymkId: this.toNumber(row.fymkId),
645
+ fymkAdi: row.fymkAdi ?? null,
646
+ fymkIlKodu: this.toNumber(row.fymkIlKodu),
647
+ fymkIlAdi: row.fymkIlAdi ?? null,
648
+ fymkIlceKodu: this.toNumber(row.fymkIlceKodu),
649
+ fymkIlceAdi: row.fymkIlceAdi ?? null,
650
+ birimAdi: row.birimAdi,
651
+ birimGrupId: this.toNumber(row.birimGrupId),
652
+ birimGrupAdi: row.birimGrupAdi ?? null,
653
+ birimTuruId: this.toNumber(row.birimTuruId),
654
+ birimTuruAdi: row.birimTuruAdi,
655
+ ogrenimTuruId: this.toNumber(row.ogrenimTuruId),
656
+ ogrenimTuruAdi: row.ogrenimTuruAdi ?? null,
657
+ ogrenimSuresi: this.toNumber(row.ogrenimSuresi),
658
+ puanTuru: row.puanTuru,
659
+ ogrenimDiliId: this.toNumber(row.ogrenimDiliId),
660
+ ogrenimDiliAdi: row.ogrenimDiliAdi ?? null,
661
+ bursOraniId: this.toNumber(row.bursOraniId),
662
+ bursOraniAdi: row.bursOraniAdi ?? null,
663
+ ilKodu: this.toNumber(row.ilKodu),
664
+ ilAdi: row.ilAdi ?? null,
665
+ ilceKodu: this.toNumber(row.ilceKodu),
666
+ ilceAdi: row.ilceAdi ?? null,
667
+ universiteTuru: row.universiteTuru,
668
+ current,
669
+ history
670
+ };
671
+ }
672
+ static buildNet(rawRow) {
673
+ const row = this.normalizeOnlisansSpelling(rawRow);
674
+ return {
675
+ yil: this.toNumber(row.yil),
676
+ kilavuzKodu: this.toNumber(row.kilavuzKodu),
677
+ puanTuru: row.puanTuru,
678
+ katsayi: this.toNumber(row.katsayi),
679
+ tabanPuan: this.toNumber(row.tabanPuan),
680
+ obp: this.toNumber(row.obp),
681
+ tytTrkNet: this.toNumber(row.tytTrkNet),
682
+ tytSosNet: this.toNumber(row.tytSosNet),
683
+ tytMatNet: this.toNumber(row.tytMatNet),
684
+ tytFenNet: this.toNumber(row.tytFenNet),
685
+ aytMatNet: this.toNumber(row.aytMatNet),
686
+ aytFizNet: this.toNumber(row.aytFizNet),
687
+ aytKimNet: this.toNumber(row.aytKimNet),
688
+ aytBioNet: this.toNumber(row.aytBioNet),
689
+ aytTdeNet: this.toNumber(row.aytTdeNet),
690
+ aytTrh1Net: this.toNumber(row.aytTrh1Net),
691
+ aytCog1Net: this.toNumber(row.aytCog1Net),
692
+ aytTrh2Net: this.toNumber(row.aytTrh2Net),
693
+ aytCog2Net: this.toNumber(row.aytCog2Net),
694
+ aytFelNet: this.toNumber(row.aytFelNet),
695
+ aytDinNet: this.toNumber(row.aytDinNet),
696
+ ydtYdilNet: this.toNumber(row.ydtYdilNet),
697
+ universiteId: this.toNumber(row.universiteId),
698
+ universiteAdi: row.universiteAdi,
699
+ birimGrupId: this.toNumber(row.birimGrupId),
700
+ birimGrupAdi: row.birimGrupAdi ?? null,
701
+ birimId: this.toNumber(row.birimId),
702
+ birimAdi: row.birimAdi,
703
+ birimTuruId: this.toNumber(row.birimTuruId),
704
+ birimTuruAdi: row.birimTuruAdi,
705
+ universiteTuru: row.universiteTuru
706
+ };
707
+ }
708
+ // ---------------------------------------------------------------------
709
+ // İç mekanizma — HTTP taşıma katmanı
710
+ // ---------------------------------------------------------------------
711
+ static async getJson(path, signal) {
712
+ return this.request(path, { method: "GET", signal });
713
+ }
714
+ static async postJson(path, body, signal) {
715
+ return this.request(path, { method: "POST", body: JSON.stringify(body), signal });
716
+ }
717
+ static async request(path, init) {
718
+ const url = `${this.baseUrl}${path}`;
719
+ let lastError;
720
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
721
+ const timeoutController = new AbortController();
722
+ const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
723
+ const onExternalAbort = () => timeoutController.abort();
724
+ init.signal?.addEventListener("abort", onExternalAbort);
725
+ try {
726
+ const response = await fetch(url, {
727
+ method: init.method,
728
+ headers: {
729
+ Accept: "application/json",
730
+ ...init.body ? { "Content-Type": "application/json" } : {},
731
+ "User-Agent": this.userAgent
732
+ },
733
+ body: init.body,
734
+ signal: timeoutController.signal
735
+ });
736
+ return await this.handleResponse(response, path);
737
+ } catch (error) {
738
+ lastError = error;
739
+ if (error instanceof YokAtlasAPIError) throw error;
740
+ if (init.signal?.aborted) {
741
+ throw new YokAtlasAPIError(`\u0130stek iptal edildi: ${path}`, { cause: error });
742
+ }
743
+ if (attempt === this.maxRetries) {
744
+ throw new YokAtlasAPIError(`Y\xD6K Atlas API iste\u011Fi ba\u015Far\u0131s\u0131z: ${path}`, { cause: error });
745
+ }
746
+ await new Promise((resolve) => setTimeout(resolve, 200 * (attempt + 1)));
747
+ } finally {
748
+ clearTimeout(timeoutId);
749
+ init.signal?.removeEventListener("abort", onExternalAbort);
750
+ }
751
+ }
752
+ throw new YokAtlasAPIError(`Y\xD6K Atlas API iste\u011Fi ba\u015Far\u0131s\u0131z: ${path}`, { cause: lastError });
753
+ }
754
+ static async handleResponse(response, path) {
755
+ if (!response.ok) {
756
+ const body = await response.text().catch(() => void 0);
757
+ const message = `Y\xD6K Atlas API hatas\u0131 ${response.status}: ${path}`;
758
+ if (response.status === 404) throw new YokAtlasNotFoundError(message, { status: response.status, body });
759
+ if (response.status === 418 || response.status === 429) throw new YokAtlasRateLimitError(message, { status: response.status, body });
760
+ throw new YokAtlasAPIError(message, { status: response.status, body });
761
+ }
762
+ try {
763
+ return await response.json();
764
+ } catch (error) {
765
+ throw new YokAtlasAPIError(`Y\xD6K Atlas API yan\u0131t\u0131 JSON olarak ayr\u0131\u015Ft\u0131r\u0131lamad\u0131: ${path}`, { status: response.status, cause: error });
766
+ }
767
+ }
768
+ };
769
+
770
+ // src/constants.ts
771
+ var BIRIM_TURU = {
772
+ LISANS: 46,
773
+ ONLISANS: 47
774
+ };
775
+ var BURS_ORANI = {
776
+ /** Ücretsiz / tam burslu. */
777
+ UCRETSIZ: 0
778
+ };
779
+ var PUAN_TURU_ETIKETLERI = {
780
+ SAY: "Say\u0131sal",
781
+ "S\xD6Z": "S\xF6zel",
782
+ EA: "E\u015Fit A\u011F\u0131rl\u0131k",
783
+ "D\u0130L": "Dil",
784
+ TYT: "TYT (\xD6n Lisans)"
785
+ };
786
+ var BIRIM_TURU_ETIKETLERI = {
787
+ LISANS: "Lisans",
788
+ ONLISANS: "\xD6n Lisans"
789
+ };
790
+ var UNIVERSITE_TURU_ETIKETLERI = {
791
+ DEVLET: "Devlet \xDCniversitesi",
792
+ VAKIF: "Vak\u0131f \xDCniversitesi",
793
+ "VAKIF MYO": "Vak\u0131f Meslek Y\xFCksekokulu"
794
+ };
795
+ function getPuanTuruLabel(puanTuru) {
796
+ if (!puanTuru) return "Bilinmiyor";
797
+ return PUAN_TURU_ETIKETLERI[puanTuru.toUpperCase()] ?? puanTuru;
798
+ }
799
+ function getBirimTuruLabel(birimTuruAdi) {
800
+ if (!birimTuruAdi) return "Bilinmiyor";
801
+ return BIRIM_TURU_ETIKETLERI[birimTuruAdi.toUpperCase()] ?? birimTuruAdi;
802
+ }
803
+ function getUniversiteTuruLabel(universiteTuru) {
804
+ if (!universiteTuru) return "Bilinmiyor";
805
+ return UNIVERSITE_TURU_ETIKETLERI[universiteTuru.toUpperCase()] ?? universiteTuru;
806
+ }
807
+
808
+ // src/index.ts
809
+ var index_default = YokAtlas;
810
+ // Annotate the CommonJS export names for ESM import in node:
811
+ 0 && (module.exports = {
812
+ BIRIM_TURU,
813
+ BIRIM_TURU_ETIKETLERI,
814
+ BURS_ORANI,
815
+ PUAN_TURU_ETIKETLERI,
816
+ UNIVERSITE_TURU_ETIKETLERI,
817
+ YokAtlas,
818
+ YokAtlasAPIError,
819
+ YokAtlasError,
820
+ YokAtlasLookupError,
821
+ YokAtlasNotFoundError,
822
+ YokAtlasRateLimitError,
823
+ YokAtlasValidationError,
824
+ getBirimTuruLabel,
825
+ getPuanTuruLabel,
826
+ getUniversiteTuruLabel
827
+ });