import path from "node:path"; import Database from "better-sqlite3"; import { KATEGORILER, kategoriBul } from "./kategoriler"; export type Program = { id: string; isim: string; universite: string; unitur: string | null; il: string | null; fakulte: string | null; tur: string; sure: number | null; sira2025: number | null; sira2024: number | null; sira2023: number | null; sira2022: number | null; sira2021: number | null; puan2025: number | null; kontenjan2025: number | null; yerlesen2025: number | null; }; export const PUAN_TURLERI = { say: "SAYISAL", ea: "EŞİT AĞIRLIK", soz: "SÖZEL", dil: "DİL", tyt: "TYT", } as const; export type PuanTuruKey = keyof typeof PUAN_TURLERI; export type RankResults = { hayal: Program[]; dengeli: Program[]; garanti: Program[]; }; // Dev'de hot-reload başına yeni bağlantı açılmasın diye global cache const globalForDb = globalThis as unknown as { yokatlasDb?: Database.Database }; function getDb(): Database.Database { if (!globalForDb.yokatlasDb) { globalForDb.yokatlasDb = new Database( path.join(process.cwd(), "data", "yokatlas.db"), { readonly: true, fileMustExist: true } ); } return globalForDb.yokatlasDb; } const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure, sira2025, sira2024, sira2023, sira2022, sira2021, puan2025, kontenjan2025, yerlesen2025`; // 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)"; /** * Kullanıcının başarı sıralamasına göre programları üç dilime ayırır. * Sıralamada küçük sayı daha iyidir; program taban sıralaması kullanıcının * sıralamasından küçükse geçen yıl oraya yerleşmek daha iyi dereceyle mümkündü * demektir (hayal), büyükse daha güvenli demektir (garanti). */ export type UniturGrubu = "devlet" | "vakif"; // Vakıf sayılan unitur değerleri (bkz. DISTINCT unitur: VAKIF, VAKIF MYO, // YURTDISI VAKIF). Devlet dışı her şey "vakıf" grubuna girer. function uniturFiltreSql(grup?: UniturGrubu): { sql: string; args: string[] } { if (grup === "devlet") return { sql: "AND unitur = ?", args: ["DEVLET"] }; if (grup === "vakif") return { sql: "AND unitur IS NOT NULL AND unitur <> ?", args: ["DEVLET"] }; return { sql: "", args: [] }; } export function searchByRank( sira: number, turKey: PuanTuruKey, opts: { il?: string; iller?: string[]; uniturGrubu?: UniturGrubu; limitPerBucket?: number; } = {} ): RankResults { const db = getDb(); const tur = PUAN_TURLERI[turKey]; const onlisans = turKey === "tyt" ? 1 : 0; const limitPerBucket = opts.limitPerBucket ?? 12; // il (tekil, geriye uyumlu) veya iller (çoklu) — ikisi de verilirse birleşir const illerListe = [ ...(opts.il ? [opts.il] : []), ...(opts.iller ?? []), ].filter((v, i, a) => v && a.indexOf(v) === i); const ilFiltre = illerListe.length > 0 ? `AND il IN (${illerListe.map(() => "?").join(",")})` : ""; const unitur = uniturFiltreSql(opts.uniturGrubu); const ilArgs = [...illerListe, ...unitur.args]; const base = `FROM programs WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql} AND ${EFEKTIF_SIRA} IS NOT NULL AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`; // hayal: kullanıcıdan daha iyi taban sıralaması (sınıra en yakın olanlar önce) const hayal = db .prepare( `SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} DESC LIMIT ?` ) .all( tur, onlisans, ...ilArgs, Math.round(sira * 0.5), sira - 1, limitPerBucket ); // dengeli: tabanı kullanıcının sıralaması civarında const dengeli = db .prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`) .all( tur, onlisans, ...ilArgs, sira, Math.round(sira * 1.4), limitPerBucket ); // garanti: tabanı belirgin şekilde altında const garanti = db .prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`) .all( tur, onlisans, ...ilArgs, Math.round(sira * 1.4) + 1, Math.round(sira * 3), limitPerBucket ); return { hayal: hayal as Program[], dengeli: dengeli as Program[], garanti: garanti as Program[], }; } export type SihirbazFacetleri = { kategoriler: { ad: string; adet: number }[]; iller: { il: string; adet: number }[]; uniturler: { grup: UniturGrubu; adet: number }[]; }; // Sihirbaz bubble'ları için erişilebilir program penceresi: hayal alt sınırından // (sira*0.5) garanti üst sınırına (sira*3) kadar. Böylece 500B sıradaki aday Tıp // bubble'ı görmez; 5B sıradaki görür. export function rankWindowFacets( sira: number, turKey: PuanTuruKey ): SihirbazFacetleri { const db = getDb(); const tur = PUAN_TURLERI[turKey]; const onlisans = turKey === "tyt" ? 1 : 0; const altSinir = Math.round(sira * 0.5); const ustSinir = Math.round(sira * 3); const base = `FROM programs WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`; // isim bazında say → JS'te kategoriye çevir (bir isim çok kategoriye girebilir) const isimSayilari = db .prepare(`SELECT isim, COUNT(*) AS adet ${base} GROUP BY isim`) .all(tur, onlisans, altSinir, ustSinir) as { isim: string; adet: number; }[]; const kategoriSay = new Map(); for (const { isim, adet } of isimSayilari) { for (const k of kategoriBul(isim)) { kategoriSay.set(k, (kategoriSay.get(k) ?? 0) + adet); } } const kategoriler = KATEGORILER.map((ad) => ({ ad, adet: kategoriSay.get(ad) ?? 0, })) .filter((k) => k.adet >= 3) // gürültüyü ele .sort((a, b) => b.adet - a.adet); const iller = ( db .prepare( `SELECT il, COUNT(*) AS adet ${base} AND il IS NOT NULL AND il <> '' GROUP BY il ORDER BY adet DESC LIMIT 15` ) .all(tur, onlisans, altSinir, ustSinir) as { il: string; adet: number; }[] ).filter((r) => r.adet > 0); const uniturSatirlari = db .prepare(`SELECT unitur, COUNT(*) AS adet ${base} GROUP BY unitur`) .all(tur, onlisans, altSinir, ustSinir) as { unitur: string | null; adet: number; }[]; let devlet = 0; let vakif = 0; for (const { unitur, adet } of uniturSatirlari) { if (unitur === "DEVLET") devlet += adet; else if (unitur) vakif += adet; } const uniturler: { grup: UniturGrubu; adet: number }[] = []; if (devlet > 0) uniturler.push({ grup: "devlet", adet: devlet }); if (vakif > 0) uniturler.push({ grup: "vakif", adet: vakif }); return { kategoriler, iller, uniturler }; }