diff --git a/src/features/liste/hooks/use-tercih-profili.ts b/src/features/liste/hooks/use-tercih-profili.ts index f76fb90..3771ac7 100644 --- a/src/features/liste/hooks/use-tercih-profili.ts +++ b/src/features/liste/hooks/use-tercih-profili.ts @@ -28,8 +28,7 @@ export type BekleyenProgram = { | "il" | "unitur" | "tur" - | "sira2025" - | "sira2024" + | "efektifSira" >; kaynak: ManuelKaynak; adaySira?: number; diff --git a/src/features/liste/liste-queries.ts b/src/features/liste/liste-queries.ts index 569f366..d716a2e 100644 --- a/src/features/liste/liste-queries.ts +++ b/src/features/liste/liste-queries.ts @@ -8,7 +8,8 @@ import type { UniturGrubu, } from "@/types/yokatlas"; -// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin. +// Tabloda kullanılmayan alanlar (fakulte, puanSon, siraSon) client'a +// serileşmesin. // Alanlar tek tek seçilir: Program'a alan eklenirse ProgramListeSatiri // üzerinden derleyici burada da karar vermeye zorlar. export function programOzetle(p: Program): ProgramListeSatiri { @@ -20,13 +21,10 @@ export function programOzetle(p: Program): ProgramListeSatiri { il: p.il, tur: p.tur, sure: p.sure, - sira2025: p.sira2025, - sira2024: p.sira2024, - sira2023: p.sira2023, - sira2022: p.sira2022, - sira2021: p.sira2021, - kontenjan2025: p.kontenjan2025, - yerlesen2025: p.yerlesen2025, + siraGecmisi: p.siraGecmisi, + efektifSira: p.efektifSira, + kontenjanSon: p.kontenjanSon, + yerlesenSon: p.yerlesenSon, }; } diff --git a/src/features/liste/liste-store.ts b/src/features/liste/liste-store.ts index 0216cd3..0b30f43 100644 --- a/src/features/liste/liste-store.ts +++ b/src/features/liste/liste-store.ts @@ -158,10 +158,8 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[] return liste; } -export function efektifSiraAl( - p: Pick, -): number | null { - return p.sira2025 ?? p.sira2024 ?? null; +export function efektifSiraAl(p: Pick): number | null { + return p.efektifSira ?? null; } /** Program satırını manuel liste kaydına çevirir. */ @@ -174,8 +172,7 @@ export function programdanManuelTercih( | "il" | "unitur" | "tur" - | "sira2025" - | "sira2024" + | "efektifSira" >, adaySira?: number | null, kaynak: ManuelKaynak = "sonuc", diff --git a/src/features/rapor/types/rapor.ts b/src/features/rapor/types/rapor.ts index 85045e3..385348d 100644 --- a/src/features/rapor/types/rapor.ts +++ b/src/features/rapor/types/rapor.ts @@ -34,8 +34,13 @@ export type RaporSonuc = { // ayıklamak için (bkz. features/rapor/rapor-queries.ts) devMock?: boolean; // Render için havuzdan zenginleştirilmiş program bilgisi - // efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok - // siraGecmisi: [2021..2025] taban sıralamaları — deterministik trend oku + // efektifSira: üretim anındaki son yılın sırası, yoksa bir önceki yılınki — + // eski kayıtlı raporlarda yok + // siraGecmisi: [ilkYil..] taban sıralamaları — deterministik trend oku. + // Uzunluk üretim anındaki veri penceresidir (eski kayıtlarda 5 eleman); + // okuyan taraf diziyi doldurmaz, yılı `ilkYil + indeks` ile bulur. + // ilkYil: dizinin ilk elemanının yılı — eski kayıtlarda yok (okurken + // lib/veri-yillari ILK_YIL) programlar: Record< string, { @@ -45,6 +50,7 @@ export type RaporSonuc = { unitur: string | null; efektifSira?: number | null; siraGecmisi?: (number | null)[]; + ilkYil?: number; } >; }; diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index 708fbc9..13b6adf 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -26,6 +26,7 @@ import { riskNotuMetni, } from "../risk"; import { trendOzetiMetni } from "../trend"; +import { ILK_YIL } from "../veri-yillari"; // Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu // ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti @@ -141,29 +142,27 @@ function normalizeEt( function trendOzetiUret(p: AdayProgram): string { // Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts) - return trendOzetiMetni([ - p.sira2021, - p.sira2022, - p.sira2023, - p.sira2024, - p.sira2025, - ]); + return trendOzetiMetni(p.siraGecmisi); } function riskNotuUret(p: AdayProgram, adaySira: number): string { - return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira)); + return riskNotuMetni(riskHesapla(p.efektifSira, adaySira)); } /** Havuz programını render için gereken bilgiye indirger. */ function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] { - const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025]; + const gecmis = p.siraGecmisi; + const gecmisVar = gecmis.some((s) => s != null); return { isim: p.isim, universite: p.universite, il: p.il, unitur: p.unitur, - efektifSira: p.sira2025 ?? p.sira2024, - siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined, + efektifSira: p.efektifSira, + siraGecmisi: gecmisVar ? gecmis : undefined, + // Dizinin çapa yılı snapshot'a yazılır: kayıt, veri penceresi sonradan + // uzasa da kendi yıllarını bilir (eski kayıtlarda yok → okurken ILK_YIL). + ilkYil: gecmisVar ? ILK_YIL : undefined, }; } @@ -183,7 +182,7 @@ function mockRapor( // Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder const hedef = dilimHedefiHesapla(havuz); const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti; - const efektif = (p: AdayProgram) => p.sira2025 ?? p.sira2024 ?? 0; + const efektif = (p: AdayProgram) => p.efektifSira ?? 0; const dilimden = (dilim: AdayProgram["dilim"], adet: number) => havuz .filter((p) => p.dilim === dilim) @@ -293,7 +292,7 @@ export async function raporUret( const havuzMap = new Map(havuz.map((p) => [p.id, p])); const efektif = (id: string) => { const p = havuzMap.get(id)!; - return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER; + return p.efektifSira ?? Number.MAX_SAFE_INTEGER; }; let secim: Record | null = null; diff --git a/src/lib/db.ts b/src/lib/db.ts index b6195d1..704b7c0 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -3,6 +3,7 @@ import path from "node:path"; import Database from "better-sqlite3"; import { KATEGORILER, kategoriBul } from "./kategoriler"; +import { EFEKTIF_SIRA_SQL, SON_YIL, YILLAR } from "./veri-yillari"; import { PUAN_TURLERI, type DilimKey, @@ -27,12 +28,46 @@ export function getDb(): Database.Database { return globalForDb.yokatlasDb; } +// Yıl ekli kolonlar yalnız burada, `YILLAR`'dan üretilerek adlandırılır; DB +// dışına yılsız `Program` çıkar. SELECT_COLS ile okunan her satır +// `programSatiri` eşleyicisinden geçmelidir (ham satır `Program` değildir). export const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure, - sira2025, sira2024, sira2023, sira2022, sira2021, - puan2025, kontenjan2025, yerlesen2025`; + ${YILLAR.map((yil) => `sira${yil} AS s${yil}`).join(", ")}, + puan${SON_YIL} AS puanSon, kontenjan${SON_YIL} AS kontenjanSon, yerlesen${SON_YIL} AS yerlesenSon`; -// 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer -export const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)"; +// Son yılın sıralaması yoksa (az yerleşen/yeni program) bir önceki yıla düşer. +// İfade indeks sözleşmesidir; tek kaynağı lib/veri-yillari.ts. +export const EFEKTIF_SIRA = EFEKTIF_SIRA_SQL; + +/** SELECT_COLS ile okunan ham satır: sıra kolonları `s` takma adıyla gelir. */ +export type ProgramHamSatiri = Omit< + Program, + "siraGecmisi" | "efektifSira" | "siraSon" +> & + Record<`s${number}`, number | null>; + +/** Ham DB satırını yılsız `Program`'a çevirir. */ +export function programSatiri(row: ProgramHamSatiri): Program { + const siraGecmisi = YILLAR.map((yil) => row[`s${yil}`] ?? null); + const siraSon = siraGecmisi[siraGecmisi.length - 1] ?? null; + return { + id: row.id, + isim: row.isim, + universite: row.universite, + unitur: row.unitur, + il: row.il, + fakulte: row.fakulte, + tur: row.tur, + sure: row.sure, + siraGecmisi, + // EFEKTIF_SIRA ile aynı kural: son yıl, yoksa bir önceki yıl + efektifSira: siraSon ?? siraGecmisi[siraGecmisi.length - 2] ?? null, + siraSon, + puanSon: row.puanSon, + kontenjanSon: row.kontenjanSon, + yerlesenSon: row.yerlesenSon, + }; +} /** * Kullanıcının başarı sıralamasına göre programları üç dilime ayırır. @@ -112,18 +147,18 @@ export function dilimAra( // hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa const yon = dilim === "hayal" ? "DESC" : "ASC"; - const programlar = db + const satirlar = db .prepare( `SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ${yon} LIMIT ? OFFSET ?` ) - .all(...args, limit, offset) as Program[]; + .all(...args, limit, offset) as ProgramHamSatiri[]; const toplam = ( db.prepare(`SELECT COUNT(*) AS adet ${base}`).get(...args) as { adet: number; } ).adet; - return { programlar, toplam }; + return { programlar: satirlar.map(programSatiri), toplam }; } export function searchByRank( diff --git a/src/lib/katalog.ts b/src/lib/katalog.ts index a2ebbf8..ae4339c 100644 --- a/src/lib/katalog.ts +++ b/src/lib/katalog.ts @@ -1,5 +1,11 @@ import "server-only"; -import { getDb, SELECT_COLS, EFEKTIF_SIRA } from "./db"; +import { + getDb, + programSatiri, + SELECT_COLS, + EFEKTIF_SIRA, + type ProgramHamSatiri, +} from "./db"; import type { Program } from "@/types/yokatlas"; import { kategoriBul, type Kategori } from "./kategoriler"; import { uniAdiNormalize } from "./harita"; @@ -24,7 +30,7 @@ export type BolumOzet = { kategoriler: Kategori[]; lisans: boolean; onlisans: boolean; - /** MIN(COALESCE(sira2025, sira2024)) — en iyi (en küçük) taban sıralaması */ + /** MIN(efektif sıra) — en iyi (en küçük) taban sıralaması */ enIyiSira: number | null; }; @@ -131,13 +137,15 @@ export function getBolumBySlug(slug: string): BolumDetay | null { if (!kayit) return null; const placeholders = kayit.hamIsimler.map(() => "?").join(","); - const programlar = getDb() - .prepare( - `SELECT ${SELECT_COLS} FROM programs - WHERE isim IN (${placeholders}) - ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, - ) - .all(...kayit.hamIsimler) as (Program & { onlisans?: number })[]; + const programlar = ( + getDb() + .prepare( + `SELECT ${SELECT_COLS} FROM programs + WHERE isim IN (${placeholders}) + ORDER BY (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, + ) + .all(...kayit.hamIsimler) as ProgramHamSatiri[] + ).map(programSatiri); // onlisans SELECT_COLS içinde yok; tur üzerinden ayır (TYT = önlisans) const lisansProgramlari = programlar.filter((p) => p.tur !== "TYT"); @@ -156,14 +164,14 @@ export function getBolumBySlug(slug: string): BolumDetay | null { if (p.unitur === "DEVLET") devlet += 1; else if (p.unitur) vakif += 1; else bilinmeyenTur += 1; - const efektif = p.sira2025 ?? p.sira2024; + const efektif = p.efektifSira; if (efektif != null) { if (minSira == null || efektif < minSira) minSira = efektif; if (maxSira == null || efektif > maxSira) maxSira = efektif; } - if (p.puan2025 != null) { - if (minPuan == null || p.puan2025 < minPuan) minPuan = p.puan2025; - if (maxPuan == null || p.puan2025 > maxPuan) maxPuan = p.puan2025; + if (p.puanSon != null) { + if (minPuan == null || p.puanSon < minPuan) minPuan = p.puanSon; + if (maxPuan == null || p.puanSon > maxPuan) maxPuan = p.puanSon; } } @@ -357,13 +365,15 @@ export function getUniversiteBySlug(slug: string): UniDetay | null { if (!kayit) return null; const placeholders = kayit.hamAdlar.map(() => "?").join(","); - const programlar = getDb() - .prepare( - `SELECT ${SELECT_COLS} FROM programs - WHERE universite IN (${placeholders}) - ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, - ) - .all(...kayit.hamAdlar) as Program[]; + const programlar = ( + getDb() + .prepare( + `SELECT ${SELECT_COLS} FROM programs + WHERE universite IN (${placeholders}) + ORDER BY fakulte, (${EFEKTIF_SIRA} IS NULL), ${EFEKTIF_SIRA} ASC`, + ) + .all(...kayit.hamAdlar) as ProgramHamSatiri[] + ).map(programSatiri); const gruplar = new Map(); let minSira: number | null = null; @@ -373,11 +383,11 @@ export function getUniversiteBySlug(slug: string): UniDetay | null { const liste = gruplar.get(anahtar); if (liste) liste.push(p); else gruplar.set(anahtar, [p]); - const efektif = p.sira2025 ?? p.sira2024; + const efektif = p.efektifSira; if (efektif != null && (minSira == null || efektif < minSira)) { minSira = efektif; } - toplamKontenjan += p.kontenjan2025 ?? 0; + toplamKontenjan += p.kontenjanSon ?? 0; } const ozet = getAllUniversiteler().find((u) => u.slug === slug)!; diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts index 5453485..7efe92f 100644 --- a/src/lib/rapor-havuzu.ts +++ b/src/lib/rapor-havuzu.ts @@ -8,6 +8,7 @@ import { } from "@/types/yokatlas"; import type { RaporParams } from "@/features/rapor/types/rapor"; import { kategoriEslesir } from "./kategoriler"; +import { SON_YIL, YILLAR } from "./veri-yillari"; export type Dilim = "hayal" | "dengeli" | "garanti"; @@ -155,6 +156,13 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc { return { havuz: dilimBasinaKirp(ilsizHam, PROMPT_LIMIT_DILIM), gevsetildi }; } +// Prompt'a veri penceresinin tamamı değil son 4 yılı gider (yeniden eskiye): +// pencere uzadıkça satır başına token sayısı sabit kalır. +const PROMPT_YIL_SAYISI = 4; +const PROMPT_YIL_INDEKSLERI = YILLAR.map((_, i) => i) + .slice(-PROMPT_YIL_SAYISI) + .reverse(); + /** Prompt'a enjekte edilecek kompakt satır (token tasarrufu). */ export function havuzuKompaktJson(havuz: AdayProgram[]): string { return JSON.stringify( @@ -165,14 +173,16 @@ export function havuzuKompaktJson(havuz: AdayProgram[]): string { unitur: p.unitur, il: p.il, dilim: p.dilim, - sira: { - y2025: p.sira2025, - y2024: p.sira2024, - y2023: p.sira2023, - y2022: p.sira2022, - }, - kontenjan2025: p.kontenjan2025, - yerlesen2025: p.yerlesen2025, + // Anahtarlar yıl taşır (y, kontenjan): model rakamın hangi + // yıla ait olduğunu anahtardan okur. + sira: Object.fromEntries( + PROMPT_YIL_INDEKSLERI.map((i) => [ + `y${YILLAR[i]}`, + p.siraGecmisi[i] ?? null, + ]), + ), + [`kontenjan${SON_YIL}`]: p.kontenjanSon, + [`yerlesen${SON_YIL}`]: p.yerlesenSon, })), ); } diff --git a/src/lib/rapor-maske.ts b/src/lib/rapor-maske.ts index ffb70ad..8842b6a 100644 --- a/src/lib/rapor-maske.ts +++ b/src/lib/rapor-maske.ts @@ -44,7 +44,7 @@ export function raporMaskele(rapor: RaporSonuc): MaskeliRapor { // kilitli metinler istemciye hiç ulaşmaz. uyarilar: rapor.uyarilar?.slice(0, 1) ?? [], kilitliUyariSayisi: Math.max(0, (rapor.uyarilar?.length ?? 0) - 1), - // Program adı, üniversite ve 2021–2025 taban sıralamaları ham veridir. + // Program adı, üniversite ve geçmiş yılların taban sıralamaları ham veridir. programlar: rapor.programlar, // Kapsam (kaç program bulundu / dağılım uyarlandı mı) dürüstlük bilgisi: // paketsiz kullanıcı da görür, maskelenmez. diff --git a/src/lib/risk.ts b/src/lib/risk.ts index 132e176..e025ac0 100644 --- a/src/lib/risk.ts +++ b/src/lib/risk.ts @@ -32,9 +32,10 @@ export const RISK_PIN_RENK: Record = { }; /** - * Deterministik risk: programın efektif taban sıralaması (COALESCE 2025→2024) - * adayın sıralamasıyla kıyaslanır. Eşikler db.ts'teki dilim aralıklarıyla - * birebir aynı (dengeli: [sira, 1.4×sira], garanti: (1.4×sira, ∞)). + * Deterministik risk: programın efektif taban sıralaması (son yılın sırası, + * yoksa bir önceki yılınki) adayın sıralamasıyla kıyaslanır. Eşikler + * db.ts'teki dilim aralıklarıyla birebir aynı (dengeli: [sira, 1.4×sira], + * garanti: (1.4×sira, ∞)). */ export function riskHesapla( efektifSira: number | null | undefined, @@ -72,7 +73,8 @@ export function dilimdenFarkliRiskEtiketi( * Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık). * Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın * sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez. - * "Geçen yıl" da denmez: efektif sıra 2025 yoksa 2024'e düşüyor. + * "Geçen yıl" da denmez: efektif sıra, son yılın verisi yoksa bir önceki + * yılınkine düşüyor. */ export function riskNotuMetni(risk: RiskSeviyesi | null): string { switch (risk) { diff --git a/src/lib/tadimlik-havuzu.ts b/src/lib/tadimlik-havuzu.ts index 7b01b7b..90071fb 100644 --- a/src/lib/tadimlik-havuzu.ts +++ b/src/lib/tadimlik-havuzu.ts @@ -10,6 +10,7 @@ import type { Dilim } from "./rapor-havuzu"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk"; import { trendOzetiMetni } from "./trend"; +import { VERI_YIL_SAYISI } from "./veri-yillari"; /** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */ export const GENEL_KATEGORI = "genel"; @@ -54,16 +55,41 @@ export type TadimlikTip = "genel" | "devlet" | "vakif"; * gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya * göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık / * ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe - * servis edilmez. Eski batch'in "son 4 yıl" kalıbı da takılır (veri 5 yılı - * kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe - * kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı - * eşlemez). + * servis edilmez. Kalıp küçük harfledir: metin önce Türkçe kurallarıyla + * küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı eşlemez). */ const YASAK_GEREKCE_DILI = - /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/; + /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil/; + +// Bayat batch kalıbı: gerekçe "son N yıl" diyorsa N, verinin bugün kapsadığı +// yıl sayısı (VERI_YIL_SAYISI) olmalı. Veri penceresi uzayınca eski batch'in +// "son 5 yıl"ı da, daha eskisinin "son 4 yıl"ı da kendiliğinden takılır. +const SON_N_YIL = /son (\d+|[a-zçğıöşü]+) yıl/g; +const SAYI_SOZCUKLERI: Record = { + bir: 1, + iki: 2, + üç: 3, + dört: 4, + beş: 5, + altı: 6, + yedi: 7, + sekiz: 8, + dokuz: 9, + on: 10, +}; + +function bayatYilSayisiVar(kucukMetin: string): boolean { + for (const [, ham] of kucukMetin.matchAll(SON_N_YIL)) { + const n = /^\d+$/.test(ham) ? Number(ham) : SAYI_SOZCUKLERI[ham]; + // Sayı olmayan sözcük ("son birkaç yıl") yıl sayısı iddiası değildir + if (n != null && n !== VERI_YIL_SAYISI) return true; + } + return false; +} export function gerekceServisEdilebilir(gerekce: string): boolean { - return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR")); + const kucuk = gerekce.toLocaleLowerCase("tr-TR"); + return !YASAK_GEREKCE_DILI.test(kucuk) && !bayatYilSayisiVar(kucuk); } /** @@ -108,10 +134,12 @@ export async function tadimlikSec( // aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı // saf fonksiyondan programın sıra geçmişinden okuma anında üretilir — // metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz. + // Snapshot'ın sıra dizisi üretildiği günün penceresidir (eski + // satırlarda 5 eleman); yıllar dizinin kendi çapasından okunur. riskNotu: riskNotuMetni( riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim), ), - trendOzeti: trendOzetiMetni(program.siraGecmisi), + trendOzeti: trendOzetiMetni(program.siraGecmisi, program.ilkYil), program, }; } diff --git a/src/lib/trend.ts b/src/lib/trend.ts index 4e3c8bd..8221cda 100644 --- a/src/lib/trend.ts +++ b/src/lib/trend.ts @@ -4,12 +4,7 @@ // düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil, // serideki dolu yıllardan gelir. -/** - * Sıra geçmişi dizisinin ilk elemanının yılı. Dizi her yerde 2021'den başlar - * (bkz. programBilgisi, raporSiraSerisi); yeni veri yılı sona eklenir, bu - * çapa değişmez. - */ -const ILK_VERI_YILI = 2021; +import { ILK_YIL } from "./veri-yillari"; /** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ function siraKisa(n: number): string { @@ -21,7 +16,10 @@ function siraKisa(n: number): string { } /** - * [2021..] taban sıralaması dizisinden tek cümlelik trend özeti. + * [ilkYil..] taban sıralaması dizisinden tek cümlelik trend özeti. Dizi + * `ilkYil`'a demirlidir (varsayılan: veri penceresinin ilk yılı); yeni veri + * yılı sona eklenir, bu yüzden eski kayıtların daha kısa dizileri de doğru + * yılları verir. `ilkYil`, snapshot kendi çapasını taşıyorsa ondan gelir. * * Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar * (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı @@ -30,22 +28,23 @@ function siraKisa(n: number): string { */ export function trendOzetiMetni( gecmis: readonly (number | null | undefined)[] | undefined, + ilkYil: number = ILK_YIL, ): string { const seri: Array<[number, number]> = []; (gecmis ?? []).forEach((sira, i) => { - if (sira != null) seri.push([ILK_VERI_YILI + i, sira]); + if (sira != null) seri.push([ilkYil + i, sira]); }); if (seri.length < 2) { return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; } - const [ilkYil, ilkSira] = seri[0]; + const [basYil, ilkSira] = seri[0]; const [sonYil, sonSira] = seri[seri.length - 1]; const oran = (sonSira - ilkSira) / ilkSira; if (oran <= -0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; + return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; } if (oran >= 0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`; + return `${basYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`; } - return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; + return `${basYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; } diff --git a/src/lib/veri-kapsami.ts b/src/lib/veri-kapsami.ts index 937f77a..f3a7f47 100644 --- a/src/lib/veri-kapsami.ts +++ b/src/lib/veri-kapsami.ts @@ -1,6 +1,6 @@ /** - * Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında - * sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu - * sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle. + * Taban sıralaması geçmişinin kapsadığı yıl sayısı; "son N yıl" diyen tüm + * metinler bu sabitten okur. Değer yıl kapsamının tek kaynağından + * (lib/veri-yillari.ts) türetilir — yeni veri yılı eklenince burası değişmez. */ -export const VERI_YIL_SAYISI = 5; +export { VERI_YIL_SAYISI } from "./veri-yillari"; diff --git a/src/types/yokatlas.ts b/src/types/yokatlas.ts index 55b1c9e..6e7d61b 100644 --- a/src/types/yokatlas.ts +++ b/src/types/yokatlas.ts @@ -11,14 +11,16 @@ export type Program = { 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; + /** Taban sıralamaları; `YILLAR` (lib/veri-yillari) ile indeks hizalı. */ + siraGecmisi: (number | null)[]; + /** Son yılın sırası; o yıl verisi yoksa bir önceki yılınki. */ + efektifSira: number | null; + // "Son" = en son yerleştirme yılı (SON_YIL). Alan adları bilinçli yılsız: + // yeni veri yılı yüklenince tip ve tüketiciler değişmez. + siraSon: number | null; + puanSon: number | null; + kontenjanSon: number | null; + yerlesenSon: number | null; }; export const PUAN_TURLERI = { @@ -42,11 +44,15 @@ export type RankResults = { }; /** - * /sonuc ham tablosunun satırı: tablo `fakulte` ve `puan2025`'i hiç + * /sonuc ham tablosunun satırı: tablo `fakulte`, `puanSon` ve `siraSon`'u + * (dizinin son elemanı; tablo `efektifSira` + `siraGecmisi` ile çalışır) hiç * kullanmadığı için bu alanlar client'a serileştirilmez (satır başına en * uzun string alanlardan biri fakulte). Katalog sayfaları tam Program kullanır. */ -export type ProgramListeSatiri = Omit; +export type ProgramListeSatiri = Omit< + Program, + "fakulte" | "puanSon" | "siraSon" +>; export type RankResultsListe = { hayal: ProgramListeSatiri[];