Greasy Fork 支持简体中文。

carrot-script

Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan

// ==UserScript==
// @name         carrot-script
// @namespace    https://greasyfork.org/zh-CN/users/1182955
// @version      0.1.1
// @author       meooow25 & RimuruChan
// @description  Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan
// @license      MIT
// @icon         https://aowuucdn.oss-accelerate.aliyuncs.com/codeforces.png
// @homepageURL  https://github.com/RimuruChan/carrot-userscript
// @match        https://codeforces.com/*
// @grant        GM.deleteValue
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// ==/UserScript==

(function () {
  'use strict';

  var __defProp = Object.defineProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  class Api {
    constructor(fetchFromContentScript2) {
      __publicField(this, "fetchFromContentScript");
      this.fetchFromContentScript = fetchFromContentScript2;
    }
    async fetch(path, queryParams) {
      let queryParamList = [];
      for (const [key, value] of Object.entries(queryParams)) {
        if (value !== void 0) {
          queryParamList.push([key, value]);
        }
      }
      return await this.fetchFromContentScript(path, queryParamList);
    }
    async contestList(gym = void 0) {
      return await this.fetch("contest.list", { gym });
    }
    async contestStandings(contestId, from = void 0, count = void 0, handles = void 0, room = void 0, showUnofficial = void 0) {
      return await this.fetch("contest.standings", {
        contestId,
        from,
        count,
        handles: handles && handles.length ? handles.join(";") : void 0,
        room,
        showUnofficial
      });
    }
    async contestRatingChanges(contestId) {
      return await this.fetch("contest.ratingChanges", { contestId });
    }
    async userRatedList(activeOnly = false) {
      return await this.fetch("user.ratedList", { activeOnly });
    }
  }
  class FFTConv {
    constructor(n) {
      __publicField(this, "n");
      __publicField(this, "wr");
      __publicField(this, "wi");
      __publicField(this, "rev");
      let k = 1;
      while (1 << k < n) {
        k++;
      }
      this.n = 1 << k;
      const n2 = this.n >> 1;
      this.wr = [];
      this.wi = [];
      const ang = 2 * Math.PI / this.n;
      for (let i = 0; i < n2; i++) {
        this.wr[i] = Math.cos(i * ang);
        this.wi[i] = Math.sin(i * ang);
      }
      this.rev = [0];
      for (let i = 1; i < this.n; i++) {
        this.rev[i] = this.rev[i >> 1] >> 1 | (i & 1) << k - 1;
      }
    }
    reverse(a) {
      for (let i = 1; i < this.n; i++) {
        if (i < this.rev[i]) {
          const tmp = a[i];
          a[i] = a[this.rev[i]];
          a[this.rev[i]] = tmp;
        }
      }
    }
    transform(ar, ai) {
      this.reverse(ar);
      this.reverse(ai);
      const wr = this.wr;
      const wi = this.wi;
      for (let len = 2; len <= this.n; len <<= 1) {
        const half = len >> 1;
        const diff = this.n / len;
        for (let i = 0; i < this.n; i += len) {
          let pw = 0;
          for (let j = i; j < i + half; j++) {
            const k = j + half;
            const vr = ar[k] * wr[pw] - ai[k] * wi[pw];
            const vi = ar[k] * wi[pw] + ai[k] * wr[pw];
            ar[k] = ar[j] - vr;
            ai[k] = ai[j] - vi;
            ar[j] += vr;
            ai[j] += vi;
            pw += diff;
          }
        }
      }
    }
    convolve(a, b) {
      if (a.length === 0 || b.length === 0) {
        return [];
      }
      const n = this.n;
      const resLen = a.length + b.length - 1;
      if (resLen > n) {
        throw new Error(
          `a.length + b.length - 1 is ${a.length} + ${b.length} - 1 = ${resLen}, expected <= ${n}`
        );
      }
      const cr = new Array(n).fill(0);
      const ci = new Array(n).fill(0);
      cr.splice(0, a.length, ...a);
      ci.splice(0, b.length, ...b);
      this.transform(cr, ci);
      cr[0] = 4 * cr[0] * ci[0];
      ci[0] = 0;
      for (let i = 1, j = n - 1; i <= j; i++, j--) {
        const ar = cr[i] + cr[j];
        const ai = ci[i] - ci[j];
        const br = ci[j] + ci[i];
        const bi = cr[j] - cr[i];
        cr[i] = ar * br - ai * bi;
        ci[i] = ar * bi + ai * br;
        cr[j] = cr[i];
        ci[j] = -ci[i];
      }
      this.transform(cr, ci);
      const res = [];
      res[0] = cr[0] / (4 * n);
      for (let i = 1, j = n - 1; i <= j; i++, j--) {
        res[i] = cr[j] / (4 * n);
        res[j] = cr[i] / (4 * n);
      }
      res.splice(resLen);
      return res;
    }
  }
  function binarySearch(left, right, predicate) {
    if (left > right) {
      throw new Error(`left ${left} must be <= right ${right}`);
    }
    while (left < right) {
      const mid = Math.floor((left + right) / 2);
      if (predicate(mid)) {
        right = mid;
      } else {
        left = mid + 1;
      }
    }
    return left;
  }
  const DEFAULT_RATING = 1400;
  class Contestant {
    constructor(handle, points, penalty, rating) {
      __publicField(this, "handle");
      __publicField(this, "points");
      __publicField(this, "penalty");
      __publicField(this, "rating");
      __publicField(this, "effectiveRating");
      __publicField(this, "rank");
      __publicField(this, "delta");
      __publicField(this, "performance");
      this.handle = handle;
      this.points = points;
      this.penalty = penalty;
      this.rating = rating;
      this.effectiveRating = rating == null ? DEFAULT_RATING : rating;
      this.rank = null;
      this.delta = null;
      this.performance = null;
    }
  }
  class PredictResult {
    constructor(handle, rating, delta, performance2) {
      __publicField(this, "handle");
      __publicField(this, "rating");
      __publicField(this, "delta");
      __publicField(this, "performance");
      this.handle = handle;
      this.rating = rating;
      this.delta = delta;
      this.performance = performance2;
    }
    get effectiveRating() {
      return this.rating == null ? DEFAULT_RATING : this.rating;
    }
  }
  const MAX_RATING_LIMIT = 6e3;
  const MIN_RATING_LIMIT = -500;
  const RATING_RANGE_LEN = MAX_RATING_LIMIT - MIN_RATING_LIMIT;
  const ELO_OFFSET = RATING_RANGE_LEN;
  const RATING_OFFSET = -MIN_RATING_LIMIT;
  const ELO_WIN_PROB = new Array(2 * RATING_RANGE_LEN + 1);
  for (let i = -RATING_RANGE_LEN; i <= RATING_RANGE_LEN; i++) {
    ELO_WIN_PROB[i + ELO_OFFSET] = 1 / (1 + Math.pow(10, i / 400));
  }
  const fftConv = new FFTConv(ELO_WIN_PROB.length + RATING_RANGE_LEN - 1);
  class RatingCalculator {
    constructor(contestants) {
      __publicField(this, "contestants");
      __publicField(this, "seed");
      __publicField(this, "adjustment");
      this.contestants = contestants;
      this.seed = null;
      this.adjustment = null;
    }
    calculateDeltas(calcPerfs = false) {
      performance.now();
      this.calcSeed();
      this.reassignRanks();
      this.calcDeltas();
      this.adjustDeltas();
      if (calcPerfs) {
        this.calcPerfs();
      }
      performance.now();
    }
    calcSeed() {
      const counts = new Array(RATING_RANGE_LEN).fill(0);
      for (const c of this.contestants) {
        counts[c.effectiveRating + RATING_OFFSET] += 1;
      }
      this.seed = fftConv.convolve(ELO_WIN_PROB, counts);
      for (let i = 0; i < this.seed.length; i++) {
        this.seed[i] += 1;
      }
    }
    getSeed(r, exclude) {
      return this.seed[r + ELO_OFFSET + RATING_OFFSET] - ELO_WIN_PROB[r - exclude + ELO_OFFSET];
    }
    reassignRanks() {
      this.contestants.sort(
        (a, b) => a.points !== b.points ? b.points - a.points : a.penalty - b.penalty
      );
      let lastPoints, lastPenalty, rank;
      for (let i = this.contestants.length - 1; i >= 0; i--) {
        const c = this.contestants[i];
        if (c.points !== lastPoints || c.penalty !== lastPenalty) {
          lastPoints = c.points;
          lastPenalty = c.penalty;
          rank = i + 1;
        }
        c.rank = rank;
      }
    }
    calcDelta(contestant, assumedRating) {
      const c = contestant;
      const seed = this.getSeed(assumedRating, c.effectiveRating);
      const midRank = Math.sqrt(c.rank * seed);
      const needRating = this.rankToRating(midRank, c.effectiveRating);
      const delta = Math.trunc((needRating - assumedRating) / 2);
      return delta;
    }
    calcDeltas() {
      for (const c of this.contestants) {
        c.delta = this.calcDelta(c, c.effectiveRating);
      }
    }
    rankToRating(rank, selfRating) {
      return binarySearch(
        2,
        MAX_RATING_LIMIT,
        (rating) => this.getSeed(rating, selfRating) < rank
      ) - 1;
    }
    adjustDeltas() {
      this.contestants.sort((a, b) => b.effectiveRating - a.effectiveRating);
      const n = this.contestants.length;
      {
        const deltaSum = this.contestants.reduce((a, b) => a + b.delta, 0);
        const inc = Math.trunc(-deltaSum / n) - 1;
        this.adjustment = inc;
        for (const c of this.contestants) {
          c.delta += inc;
        }
      }
      {
        const zeroSumCount = Math.min(4 * Math.round(Math.sqrt(n)), n);
        const deltaSum = this.contestants.slice(0, zeroSumCount).reduce((a, b) => a + b.delta, 0);
        const inc = Math.min(Math.max(Math.trunc(-deltaSum / zeroSumCount), -10), 0);
        this.adjustment += inc;
        for (const c of this.contestants) {
          c.delta += inc;
        }
      }
    }
    calcPerfs() {
      for (const c of this.contestants) {
        if (c.rank === 1) {
          c.performance = Infinity;
        } else {
          c.performance = binarySearch(
            MIN_RATING_LIMIT,
            MAX_RATING_LIMIT,
            (assumedRating) => this.calcDelta(c, assumedRating) + this.adjustment <= 0
          );
        }
      }
    }
  }
  function predict$1(contestants, calcPerfs = false) {
    new RatingCalculator(contestants).calculateDeltas(calcPerfs);
    return contestants.map((c) => new PredictResult(c.handle, c.rating, c.delta, c.performance));
  }
  const _Rank = class _Rank {
    constructor(name, abbr, low, high, colorClass) {
      __publicField(this, "name");
      __publicField(this, "abbr");
      __publicField(this, "low");
      __publicField(this, "high");
      __publicField(this, "colorClass");
      this.name = name;
      this.abbr = abbr;
      this.low = low;
      this.high = high;
      this.colorClass = colorClass;
    }
    static forRating(rating) {
      if (rating == null) {
        return _Rank.UNRATED;
      }
      for (const rank of _Rank.RATED) {
        if (rating < rank.high) {
          return rank;
        }
      }
      return _Rank.RATED[_Rank.RATED.length - 1];
    }
  };
  __publicField(_Rank, "UNRATED");
  __publicField(_Rank, "RATED");
  let Rank = _Rank;
  Rank.UNRATED = new Rank("Unrated", "U", -Infinity, null, null);
  Rank.RATED = [
    new Rank("Newbie", "N", -Infinity, 1200, "user-gray"),
    new Rank("Pupil", "P", 1200, 1400, "user-green"),
    new Rank("Specialist", "S", 1400, 1600, "user-cyan"),
    new Rank("Expert", "E", 1600, 1900, "user-blue"),
    new Rank("Candidate Master", "CM", 1900, 2100, "user-violet"),
    new Rank("Master", "M", 2100, 2300, "user-orange"),
    new Rank("International Master", "IM", 2300, 2400, "user-orange"),
    new Rank("Grandmaster", "GM", 2400, 2600, "user-red"),
    new Rank("International Grandmaster", "IGM", 2600, 3e3, "user-red"),
    new Rank("Legendary Grandmaster", "LGM", 3e3, 4e3, "user-legendary"),
    new Rank("Tourist", "T", 4e3, Infinity, "user-4000")
  ];
  class PredictResponseRow {
    constructor(delta, rank, performance2, newRank, deltaReqForRankUp, nextRank) {
      __publicField(this, "delta");
      __publicField(this, "rank");
      __publicField(this, "performance");
      // For FINAL
      __publicField(this, "newRank");
      // For PREDICTED
      __publicField(this, "deltaReqForRankUp");
      __publicField(this, "nextRank");
      this.delta = delta;
      this.rank = rank;
      this.performance = performance2;
      this.newRank = newRank;
      this.deltaReqForRankUp = deltaReqForRankUp;
      this.nextRank = nextRank;
    }
  }
  const _PredictResponse = class _PredictResponse {
    constructor(predictResults, type, fetchTime) {
      __publicField(this, "rowMap");
      __publicField(this, "type");
      __publicField(this, "fetchTime");
      _PredictResponse.assertTypeOk(type);
      this.rowMap = {};
      this.type = type;
      this.fetchTime = fetchTime;
      this.populateMap(predictResults);
    }
    populateMap(predictResults) {
      for (const result of predictResults) {
        let rank, newRank, deltaReqForRankUp, nextRank;
        switch (this.type) {
          case _PredictResponse.TYPE_PREDICTED:
            rank = Rank.forRating(result.rating);
            const effectiveRank = Rank.forRating(result.effectiveRating);
            deltaReqForRankUp = effectiveRank.high - result.effectiveRating;
            nextRank = Rank.RATED[Rank.RATED.indexOf(effectiveRank) + 1] || null;
            break;
          case _PredictResponse.TYPE_FINAL:
            rank = Rank.forRating(result.rating);
            newRank = Rank.forRating(result.effectiveRating + result.delta);
            break;
          default:
            throw new Error("Unknown prediction type");
        }
        const performance2 = {
          value: result.performance === Infinity ? "Infinity" : result.performance,
          colorClass: Rank.forRating(result.performance).colorClass
        };
        this.rowMap[result.handle] = new PredictResponseRow(
          result.delta,
          rank,
          performance2,
          newRank,
          deltaReqForRankUp,
          nextRank
        );
      }
    }
    static assertTypeOk(type) {
      if (!_PredictResponse.TYPES.includes(type)) {
        throw new Error("Unknown prediction type: " + type);
      }
    }
  };
  __publicField(_PredictResponse, "TYPE_PREDICTED", "PREDICTED");
  __publicField(_PredictResponse, "TYPE_FINAL", "FINAL");
  __publicField(_PredictResponse, "TYPES", [_PredictResponse.TYPE_PREDICTED, _PredictResponse.TYPE_FINAL]);
  let PredictResponse = _PredictResponse;
  class Lock {
    constructor() {
      __publicField(this, "queue");
      __publicField(this, "locked");
      this.queue = [];
      this.locked = false;
    }
    async acquire() {
      if (this.locked) {
        await new Promise((resolve) => {
          this.queue.push(resolve);
        });
      }
      this.locked = true;
    }
    release() {
      if (!this.locked) {
        throw new Error("The lock must be acquired before release");
      }
      this.locked = false;
      if (this.queue.length) {
        const resolve = this.queue.shift();
        resolve();
      }
    }
    async execute(asyncFunc) {
      await this.acquire();
      try {
        return await asyncFunc();
      } finally {
        this.release();
      }
    }
  }
  const REFRESH_INTERVAL = 6 * 60 * 60 * 1e3;
  const CONTESTS$1 = "cache.contests";
  const CONTESTS_TIMESTAMP = "cache.contests.timestamp";
  class Contests {
    constructor(api, storage) {
      __publicField(this, "api");
      __publicField(this, "storage");
      __publicField(this, "lock");
      this.api = api;
      this.storage = storage;
      this.lock = new Lock();
    }
    async getLastAttemptTime() {
      return await this.storage.get(CONTESTS_TIMESTAMP, 0);
    }
    async setLastAttemptTime(time) {
      await this.storage.set(CONTESTS_TIMESTAMP, time);
    }
    async getContestMap() {
      let res = await this.storage.get(CONTESTS$1, {});
      res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), v]));
      return res;
    }
    async setContestMap(contestMap) {
      const obj = Object.fromEntries(contestMap);
      await this.storage.set(CONTESTS$1, obj);
    }
    async maybeRefreshCache() {
      const inner = async () => {
        const now = Date.now();
        const refresh = now - await this.getLastAttemptTime() > REFRESH_INTERVAL;
        if (!refresh) {
          return;
        }
        await this.setLastAttemptTime(now);
        try {
          const contests = await this.api.contestList();
          await this.setContestMap(new Map(contests.map((c) => [c.id, c])));
        } catch (er) {
          console.warn("Unable to fetch contest list: " + er);
        }
      };
      await this.lock.execute(inner);
    }
    async list() {
      return Array.from((await this.getContestMap()).values());
    }
    async hasCached(contestId) {
      return (await this.getContestMap()).has(contestId);
    }
    async getCached(contestId) {
      return (await this.getContestMap()).get(contestId);
    }
    async update(contest) {
      const contestMap = await this.getContestMap();
      contestMap.set(contest.id, contest);
      await this.setContestMap(contestMap);
    }
  }
  const PREFETCH_INTERVAL = 60 * 60 * 1e3;
  const RATINGS_TIMESTAMP = "cache.ratings.timestamp";
  const RATINGS$1 = "cache.ratings";
  class Ratings {
    constructor(api, storage) {
      __publicField(this, "api");
      __publicField(this, "storage");
      __publicField(this, "lock");
      this.api = api;
      this.storage = storage;
      this.lock = new Lock();
    }
    async maybeRefreshCache(contestStartMs) {
      const inner = async () => {
        const timeLeft = contestStartMs - Date.now();
        if (timeLeft > PREFETCH_INTERVAL) {
          return;
        }
        const timeLeftAfterLastFetch = contestStartMs - await this.storage.get(RATINGS_TIMESTAMP, 0);
        if (timeLeftAfterLastFetch > PREFETCH_INTERVAL) {
          await this.cacheRatings();
        }
      };
      await this.lock.execute(inner);
    }
    async fetchCurrentRatings(contestStartMs) {
      if (Date.now() < contestStartMs) {
        throw new Error("getCurrentRatings should be called after contest start");
      }
      await this.maybeRefreshCache(contestStartMs);
      const ratings = await this.storage.get(RATINGS$1);
      return new Map(Object.entries(ratings));
    }
    async cacheRatings() {
      const users = await this.api.userRatedList(false);
      const ratings = Object.fromEntries(users.map((u) => [u.handle, u.rating]));
      await this.storage.set(RATINGS$1, ratings);
      await this.storage.set(RATINGS_TIMESTAMP, Date.now());
    }
  }
  const _Contest = class _Contest {
    constructor(contest, problems, rows, ratingChanges, oldRatings, fetchTime, isRated) {
      __publicField(this, "contest");
      __publicField(this, "problems");
      __publicField(this, "rows");
      __publicField(this, "ratingChanges");
      __publicField(this, "oldRatings");
      __publicField(this, "performances");
      __publicField(this, "fetchTime");
      __publicField(this, "isRated");
      __publicField(this, "startTimeSeconds");
      __publicField(this, "durationSeconds");
      this.contest = contest;
      this.problems = problems;
      this.rows = rows;
      this.ratingChanges = ratingChanges;
      this.oldRatings = oldRatings;
      this.fetchTime = fetchTime;
      this.isRated = isRated;
      this.performances = null;
      this.startTimeSeconds = 0;
      this.durationSeconds = 0;
    }
    toPlainObject() {
      return {
        contest: this.contest,
        problems: this.problems,
        rows: this.rows,
        ratingChanges: this.ratingChanges,
        oldRatings: Array.from(this.oldRatings),
        fetchTime: this.fetchTime,
        isRated: this.isRated
      };
    }
    static fromPlainObject(obj) {
      const c = new _Contest(
        obj.contest,
        obj.problems,
        obj.rows,
        obj.ratingChanges,
        new Map(obj.oldRatings),
        obj.fetchTime,
        obj.isRated
      );
      return c;
    }
  };
  __publicField(_Contest, "IsRated", {
    YES: "YES",
    NO: "NO",
    LIKELY: "LIKELY"
  });
  let Contest = _Contest;
  const MAGIC_CACHE_DURATION = 5 * 60 * 1e3;
  const RATING_PENDING_MAX_DAYS = 3;
  function isOldContest(contest) {
    const daysSinceContestEnd = (Date.now() / 1e3 - contest.startTimeSeconds - contest.durationSeconds) / (60 * 60 * 24);
    return daysSinceContestEnd > RATING_PENDING_MAX_DAYS;
  }
  function isMagicOn() {
    let now = /* @__PURE__ */ new Date();
    return now.getMonth() === 11 && now.getDate() >= 24 || now.getMonth() === 0 && now.getDate() <= 11;
  }
  const MAX_FINISHED_CONTESTS_TO_CACHE = 5;
  const CONTESTS_COMPLETE$1 = "cache.contests_complete";
  const CONTESTS_COMPLETE_IDS = "cache.contests_complete.ids";
  const CONTESTS_COMPLETE_TIMESTAMP = "cache.contests_complete.timestamp";
  class ContestsComplete {
    constructor(api, storage) {
      __publicField(this, "api");
      __publicField(this, "storage");
      this.api = api;
      this.storage = storage;
    }
    async getContests() {
      let res = await this.storage.get(CONTESTS_COMPLETE$1, {});
      res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), Contest.fromPlainObject(v)]));
      return res;
    }
    async setContests(contests) {
      const obj = Object.fromEntries([...contests.entries()].map(([k, v]) => [k, v.toPlainObject()]));
      await this.storage.set(CONTESTS_COMPLETE$1, obj);
    }
    async getContestIds() {
      return await this.storage.get(CONTESTS_COMPLETE_IDS, []);
    }
    async setContestIds(contestIds) {
      await this.storage.set(CONTESTS_COMPLETE_IDS, contestIds);
    }
    async getContestTimestamp() {
      let res = await this.storage.get(CONTESTS_COMPLETE_TIMESTAMP, {});
      res = new Map(Object.entries(res));
      return res;
    }
    async setContestTimestamp(contestTimestamp) {
      const obj = Object.fromEntries(contestTimestamp);
      await this.storage.set(CONTESTS_COMPLETE_TIMESTAMP, obj);
    }
    async fetch(contestId) {
      const cachedContests = await this.getContests();
      if (cachedContests.has(contestId)) {
        console.log("Returning cached contest");
        return cachedContests.get(contestId);
      }
      const { contest, problems, rows } = await this.api.contestStandings(contestId);
      let ratingChanges;
      let oldRatings;
      let isRated = Contest.IsRated.LIKELY;
      if (contest.phase === "FINISHED") {
        try {
          ratingChanges = await this.api.contestRatingChanges(contestId);
          if (ratingChanges) {
            if (ratingChanges.length > 0) {
              isRated = Contest.IsRated.YES;
              oldRatings = adjustOldRatings(contestId, ratingChanges);
            } else {
              ratingChanges = void 0;
            }
          }
        } catch (er) {
          if (er.message.includes("Rating changes are unavailable for this contest")) {
            isRated = Contest.IsRated.NO;
          }
        }
      }
      if (isRated === Contest.IsRated.LIKELY && isOldContest(contest)) {
        isRated = Contest.IsRated.NO;
      }
      const isFinished = isRated === Contest.IsRated.NO || isRated === Contest.IsRated.YES;
      const c = new Contest(contest, problems, rows, ratingChanges, oldRatings, Date.now(), isRated);
      if (isFinished) {
        const contests = await this.getContests();
        contests.set(contestId, c);
        let contestIds = await this.getContestIds();
        contestIds.push(contestId);
        while (contestIds.length > MAX_FINISHED_CONTESTS_TO_CACHE) {
          contests.delete(contestIds.shift());
        }
        if (isMagicOn()) {
          const contestTimestamp = await this.getContestTimestamp();
          for (const [cid, timestamp] of contestTimestamp) {
            if (Date.now() - timestamp > MAGIC_CACHE_DURATION) {
              contestTimestamp.delete(cid);
              contests.delete(cid);
              contestIds = contestIds.filter((c2) => c2 !== cid);
            }
          }
          contestTimestamp.set(contestId, Date.now());
          await this.setContestTimestamp(contestTimestamp);
        }
        await this.setContests(contests);
        await this.setContestIds(contestIds);
      }
      return c;
    }
  }
  const FAKE_RATINGS_SINCE_CONTEST = 1360;
  const NEW_DEFAULT_RATING = 1400;
  function adjustOldRatings(contestId, ratingChanges) {
    const oldRatings = /* @__PURE__ */ new Map();
    if (contestId < FAKE_RATINGS_SINCE_CONTEST) {
      for (const change of ratingChanges) {
        oldRatings.set(change.handle, change.oldRating);
      }
    } else {
      for (const change of ratingChanges) {
        oldRatings.set(change.handle, change.oldRating == 0 ? NEW_DEFAULT_RATING : change.oldRating);
      }
    }
    return oldRatings;
  }
  var _GM_addStyle = /* @__PURE__ */ (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)();
  var _GM_deleteValue = /* @__PURE__ */ (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)();
  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_listValues = /* @__PURE__ */ (() => typeof GM_listValues != "undefined" ? GM_listValues : void 0)();
  var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  class StorageWrapper {
    constructor(storageName) {
      __publicField(this, "storageName");
      this.storageName = storageName;
    }
    async get(key, defaultValue = void 0) {
      return await _GM_getValue(`${this.storageName}.${key}`, defaultValue);
    }
    async set(key, value) {
      return await _GM_setValue(`${this.storageName}.${key}`, value);
    }
  }
  const LOCAL = new StorageWrapper("LOCAL");
  const SYNC = new StorageWrapper("SYNC");
  function boolSetterGetter(key, defaultValue) {
    return async (value) => {
      if (value === void 0) {
        return await SYNC.get(key, defaultValue);
      }
      return await SYNC.set(key, value);
    };
  }
  const enablePredictDeltas = boolSetterGetter("settings.enablePredictDeltas", true);
  const enableFinalDeltas = boolSetterGetter("settings.enableFetchDeltas", true);
  const enablePrefetchRatings = boolSetterGetter("settings.enablePrefetchRatings", true);
  const showColCurrentPerformance = boolSetterGetter("settings.showColCurrentPerformance", true);
  const showColPredictedDelta = boolSetterGetter("settings.showColPredictedDelta", true);
  const showColRankUpDelta = boolSetterGetter("settings.showColRankUpDelta", true);
  const showColFinalPerformance = boolSetterGetter("settings.showColFinalPerformance", true);
  const showColFinalDelta = boolSetterGetter("settings.showColFinalDelta", true);
  const showColRankChange = boolSetterGetter("settings.showColRankChange", true);
  async function getPrefs() {
    return {
      enablePredictDeltas: await enablePredictDeltas(),
      enableFinalDeltas: await enableFinalDeltas(),
      enablePrefetchRatings: await enablePrefetchRatings(),
      showColCurrentPerformance: await showColCurrentPerformance(),
      showColPredictedDelta: await showColPredictedDelta(),
      showColRankUpDelta: await showColRankUpDelta(),
      showColFinalPerformance: await showColFinalPerformance(),
      showColFinalDelta: await showColFinalDelta(),
      showColRankChange: await showColRankChange()
    };
  }
  const UNRATED_HINTS = ["unrated", "fools", "q#", "kotlin", "marathon", "teams"];
  const EDU_ROUND_RATED_THRESHOLD = 2100;
  const API = new Api(fetchFromContentScript);
  const CONTESTS = new Contests(API, LOCAL);
  const RATINGS = new Ratings(API, LOCAL);
  const CONTESTS_COMPLETE = new ContestsComplete(API, LOCAL);
  const API_PATH = "/api/";
  async function fetchFromContentScript(path, queryParamList) {
    const url = new URL(location.origin + API_PATH + path);
    for (const [key, value] of queryParamList) {
      url.searchParams.append(key, value);
    }
    const resp = await fetch(url);
    const text = await resp.text();
    if (resp.status !== 200) {
      throw new Error(`CF API: HTTP error ${resp.status}: ${text}`);
    }
    let json;
    try {
      json = JSON.parse(text);
    } catch (_) {
      throw new Error(`CF API: Invalid JSON: ${text}`);
    }
    if (json.status !== "OK" || json.result === void 0) {
      throw new Error(`CF API: Error: ${text}`);
    }
    return json.result;
  }
  function isUnratedByName(contestName) {
    const lower = contestName.toLowerCase();
    return UNRATED_HINTS.some((hint) => lower.includes(hint));
  }
  function anyRowHasTeam(rows) {
    return rows.some((row) => row.party.teamId != null || row.party.teamName != null);
  }
  async function getDeltas(contestId) {
    const prefs = await getPrefs();
    return await calcDeltas(contestId, prefs);
  }
  async function calcDeltas(contestId, prefs) {
    if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) {
      return { result: "DISABLED" };
    }
    if (await CONTESTS.hasCached(contestId)) {
      const contest2 = await CONTESTS.getCached(contestId);
      if (isUnratedByName(contest2.name)) {
        return { result: "UNRATED_CONTEST" };
      }
    }
    const contest = await CONTESTS_COMPLETE.fetch(contestId);
    CONTESTS.update(contest.contest);
    if (contest.isRated === Contest.IsRated.NO) {
      return { result: "UNRATED_CONTEST" };
    }
    if (contest.isRated === Contest.IsRated.YES) {
      if (!prefs.enableFinalDeltas) {
        return { result: "DISABLED" };
      }
      return {
        result: "OK",
        prefs,
        predictResponse: getFinal(contest)
      };
    }
    if (isUnratedByName(contest.contest.name)) {
      return { result: "UNRATED_CONTEST" };
    }
    if (anyRowHasTeam(contest.rows)) {
      return { result: "UNRATED_CONTEST" };
    }
    if (!prefs.enablePredictDeltas) {
      return { result: "DISABLED" };
    }
    return {
      result: "OK",
      prefs,
      predictResponse: await getPredicted(contest)
    };
  }
  function predictForRows(rows, ratingBeforeContest) {
    const contestants = rows.map((row) => {
      const handle = row.party.members[0].handle;
      return new Contestant(handle, row.points, row.penalty, ratingBeforeContest.get(handle) ?? null);
    });
    return predict$1(contestants, true);
  }
  function getFinal(contest) {
    if (contest.performances === null) {
      const ratingBeforeContest = new Map(
        contest.ratingChanges.map((c) => [c.handle, contest.oldRatings.get(c.handle)])
      );
      const rows = contest.rows.filter((row) => {
        const handle = row.party.members[0].handle;
        return ratingBeforeContest.has(handle);
      });
      const predictResultsForPerf = predictForRows(rows, ratingBeforeContest);
      contest.performances = new Map(predictResultsForPerf.map((r) => [r.handle, r.performance]));
    }
    const predictResults = [];
    for (const change of contest.ratingChanges) {
      predictResults.push(
        new PredictResult(
          change.handle,
          change.oldRating,
          change.newRating - change.oldRating,
          contest.performances.get(change.handle)
        )
      );
    }
    return new PredictResponse(predictResults, PredictResponse.TYPE_FINAL, contest.fetchTime);
  }
  async function getPredicted(contest) {
    const ratingMap = await RATINGS.fetchCurrentRatings(contest.contest.startTimeSeconds * 1e3);
    const isEduRound = contest.contest.name.toLowerCase().includes("educational");
    let rows = contest.rows;
    if (isEduRound) {
      rows = contest.rows.filter((row) => {
        const handle = row.party.members[0].handle;
        return !ratingMap.has(handle) || ratingMap.get(handle) < EDU_ROUND_RATED_THRESHOLD;
      });
    }
    const predictResults = predictForRows(rows, ratingMap);
    return new PredictResponse(predictResults, PredictResponse.TYPE_PREDICTED, contest.fetchTime);
  }
  async function predictDeltas(contestId) {
    return await getDeltas(contestId);
  }
  async function maybeUpdateContestList() {
    const prefs = await getPrefs();
    if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) {
      return;
    }
    await CONTESTS.maybeRefreshCache();
  }
  async function getNearestUpcomingRatedContestStartTime() {
    let nearest = null;
    const now = Date.now();
    for (const c of await CONTESTS.list()) {
      const start = (c.startTimeSeconds || 0) * 1e3;
      if (start < now || isUnratedByName(c.name)) {
        continue;
      }
      if (nearest === null || start < nearest) {
        nearest = start;
      }
    }
    return nearest;
  }
  async function maybeUpdateRatings() {
    const prefs = await getPrefs();
    if (!prefs.enablePredictDeltas || !prefs.enablePrefetchRatings) {
      return;
    }
    const startTimeMs = await getNearestUpcomingRatedContestStartTime();
    if (startTimeMs !== null) {
      await RATINGS.maybeRefreshCache(startTimeMs);
    }
  }
  const contentCss = ".carrot-display-none {\n  display: none;\n}\n";
  const PING_INTERVAL = 3 * 60 * 1e3;
  const PREDICT_TEXT_ID = "carrot-predict-text";
  const DISPLAY_NONE_CLS = "carrot-display-none";
  const Unicode = {
    BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW: "⮭",
    GREEK_CAPITAL_DELTA: "Δ",
    GREEK_CAPITAL_PI: "Π",
    INFINITY: "∞",
    SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL: "⭜",
    BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL: "⭝"
  };
  const PREDICT_COLUMNS = [
    {
      text: "current performance",
      id: "carrot-current-performance",
      setting: "showColCurrentPerformance"
    },
    {
      text: "predicted delta",
      id: "carrot-predicted-delta",
      setting: "showColPredictedDelta"
    },
    {
      text: "delta required to rank up",
      id: "carrot-rank-up-delta",
      setting: "showColRankUpDelta"
    }
  ];
  const FINAL_COLUMNS = [
    {
      text: "final performance",
      id: "carrot-final-performance",
      setting: "showColFinalPerformance"
    },
    {
      text: "final delta",
      id: "carrot-final-delta",
      setting: "showColFinalDelta"
    },
    {
      text: "rank change",
      id: "carrot-rank-change",
      setting: "showColRankChange"
    }
  ];
  const ALL_COLUMNS = PREDICT_COLUMNS.concat(FINAL_COLUMNS);
  function makeGreySpan(text, title) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    span.style.color = "lightgrey";
    span.textContent = text;
    if (title) {
      span.title = title;
    }
    span.classList.add("small");
    return span;
  }
  function makePerformanceSpan(performance2) {
    const span = document.createElement("span");
    if (performance2.value === "Infinity") {
      span.textContent = Unicode.INFINITY;
    } else {
      span.textContent = performance2.value;
      span.classList.add(performance2.colorClass);
    }
    span.style.fontWeight = "bold";
    span.style.display = "inline-block";
    return span;
  }
  function makeRankSpan(rank) {
    const span = document.createElement("span");
    if (rank.colorClass) {
      span.classList.add(rank.colorClass);
    }
    span.style.verticalAlign = "middle";
    span.textContent = rank.abbr;
    span.title = rank.name;
    span.style.display = "inline-block";
    return span;
  }
  function makeArrowSpan(arrow) {
    const span = document.createElement("span");
    span.classList.add("small");
    span.style.verticalAlign = "middle";
    span.style.paddingLeft = "0.5em";
    span.style.paddingRight = "0.5em";
    span.textContent = arrow;
    return span;
  }
  function makeDeltaSpan(delta) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    span.style.verticalAlign = "middle";
    if (delta > 0) {
      span.style.color = "green";
      span.textContent = `+${delta}`;
    } else {
      span.style.color = "gray";
      span.textContent = delta.toString();
    }
    return span;
  }
  function makeFinalRankUpSpan(rank, newRank, arrow) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    span.appendChild(makeRankSpan(rank));
    span.appendChild(makeArrowSpan(arrow));
    span.appendChild(makeRankSpan(newRank));
    return span;
  }
  function makePredictedRankUpSpan(rank, deltaReqForRankUp, nextRank) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    if (nextRank === null) {
      span.appendChild(makeRankSpan(rank));
      return span;
    }
    span.appendChild(makeDeltaSpan(deltaReqForRankUp));
    span.appendChild(makeArrowSpan(Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL));
    span.appendChild(makeRankSpan(nextRank));
    return span;
  }
  function makePerfHeaderCell() {
    const cell = document.createElement("th");
    cell.classList.add("top");
    cell.style.width = "4em";
    {
      const span = document.createElement("span");
      span.textContent = Unicode.GREEK_CAPITAL_PI;
      span.title = "Performance";
      cell.appendChild(span);
    }
    return cell;
  }
  function makeDeltaHeaderCell(deltaColTitle) {
    const cell = document.createElement("th");
    cell.classList.add("top");
    cell.style.width = "4.5em";
    {
      const span = document.createElement("span");
      span.textContent = Unicode.GREEK_CAPITAL_DELTA;
      span.title = deltaColTitle;
      cell.appendChild(span);
    }
    cell.appendChild(document.createElement("br"));
    {
      const span = document.createElement("span");
      span.classList.add("small");
      span.id = PREDICT_TEXT_ID;
      cell.appendChild(span);
    }
    return cell;
  }
  function makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle) {
    const cell = document.createElement("th");
    cell.classList.add("top", "right");
    cell.style.width = rankUpColWidth;
    {
      const span = document.createElement("span");
      span.textContent = Unicode.BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW;
      span.title = rankUpColTitle;
      cell.appendChild(span);
    }
    return cell;
  }
  function makeDataCell(bottom = false, right = false) {
    const cell = document.createElement("td");
    if (bottom) {
      cell.classList.add("bottom");
    }
    if (right) {
      cell.classList.add("right");
    }
    return cell;
  }
  function populateCells(row, type, rankUpTint, perfCell, deltaCell, rankUpCell) {
    if (row === void 0) {
      perfCell.appendChild(makeGreySpan("N/A", "Not applicable"));
      deltaCell.appendChild(makeGreySpan("N/A", "Not applicable"));
      rankUpCell.appendChild(makeGreySpan("N/A", "Not applicable"));
      return;
    }
    perfCell.appendChild(makePerformanceSpan(row.performance));
    deltaCell.appendChild(makeDeltaSpan(row.delta));
    switch (type) {
      case "FINAL":
        if (row.rank.abbr === row.newRank.abbr) {
          rankUpCell.appendChild(makeGreySpan("N/C", "No change"));
        } else {
          const arrow = row.delta > 0 ? Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL : Unicode.BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL;
          rankUpCell.appendChild(makeFinalRankUpSpan(row.rank, row.newRank, arrow));
        }
        break;
      case "PREDICTED":
        rankUpCell.appendChild(
          makePredictedRankUpSpan(row.rank, row.deltaReqForRankUp, row.nextRank)
        );
        if (row.delta >= row.deltaReqForRankUp) {
          const [color, priority] = rankUpTint;
          rankUpCell.style.setProperty("background-color", color ?? null, priority);
        }
        break;
      default:
        throw new Error("Unknown prediction type");
    }
  }
  function updateStandings(resp) {
    let deltaColTitle, rankUpColWidth, rankUpColTitle, columns;
    switch (resp.type) {
      case "FINAL":
        deltaColTitle = "Final rating change";
        rankUpColWidth = "6.5em";
        rankUpColTitle = "Rank change";
        columns = FINAL_COLUMNS;
        break;
      case "PREDICTED":
        deltaColTitle = "Predicted rating change";
        rankUpColWidth = "7.5em";
        rankUpColTitle = "Rating change for rank up";
        columns = PREDICT_COLUMNS;
        break;
      default:
        throw new Error("Unknown prediction type");
    }
    const rows = Array.from(document.querySelectorAll("table.standings tbody tr"));
    for (const [idx, tableRow] of rows.entries()) {
      tableRow.querySelector("th:last-child, td:last-child").classList.remove("right");
      let perfCell, deltaCell, rankUpCell;
      if (idx === 0) {
        perfCell = makePerfHeaderCell();
        deltaCell = makeDeltaHeaderCell(deltaColTitle);
        rankUpCell = makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle);
      } else if (idx === rows.length - 1) {
        perfCell = makeDataCell(true);
        deltaCell = makeDataCell(true);
        rankUpCell = makeDataCell(true, true);
      } else {
        perfCell = makeDataCell();
        deltaCell = makeDataCell();
        rankUpCell = makeDataCell(false, true);
        const handle = tableRow.querySelector("td.contestant-cell").textContent.trim();
        let rankUpTint;
        if (tableRow.classList.contains("highlighted-row")) {
          rankUpTint = ["#d1eef2", "important"];
        } else {
          rankUpTint = [idx % 2 ? "#ebf8eb" : "#f2fff2", void 0];
        }
        populateCells(resp.rowMap[handle], resp.type, rankUpTint, perfCell, deltaCell, rankUpCell);
      }
      const cells = [perfCell, deltaCell, rankUpCell];
      for (let i = 0; i < cells.length; i++) {
        const cell = cells[i];
        if (idx % 2) {
          cell.classList.add("dark");
        }
        cell.classList.add(columns[i].id, DISPLAY_NONE_CLS);
        tableRow.appendChild(cell);
      }
    }
    return columns;
  }
  function updateColumnVisibility(prefs) {
    for (const col of ALL_COLUMNS) {
      const showCol = prefs[col.setting];
      const func = showCol ? (cell) => cell.classList.remove(DISPLAY_NONE_CLS) : (cell) => cell.classList.add(DISPLAY_NONE_CLS);
      document.querySelectorAll(`.${col.id}`).forEach(func);
    }
  }
  function showFinal() {
    const predictTextSpan = document.getElementById(PREDICT_TEXT_ID);
    predictTextSpan.textContent = "Final";
  }
  function showTimer(fetchTime) {
    const predictTextSpan = document.getElementById(PREDICT_TEXT_ID);
    function update() {
      const secSincePredict = Math.floor((Date.now() - fetchTime) / 1e3);
      if (secSincePredict < 30) {
        predictTextSpan.textContent = "Just now";
      } else if (secSincePredict < 60) {
        predictTextSpan.textContent = "<1m old";
      } else {
        predictTextSpan.textContent = Math.floor(secSincePredict / 60) + "m old";
      }
    }
    update();
    setInterval(update, 1e3);
  }
  async function predict(contestId) {
    const response = await predictDeltas(contestId);
    switch (response.result) {
      case "OK":
        break;
      case "UNRATED_CONTEST":
        console.info("[Carrot] Unrated contest, not displaying delta column.");
        return;
      case "DISABLED":
        console.info("[Carrot] Deltas for this contest are disabled according to user settings.");
        return;
      default:
        throw new Error("Unknown result");
    }
    const columns = updateStandings(response.predictResponse);
    switch (response.predictResponse.type) {
      case "FINAL":
        showFinal();
        break;
      case "PREDICTED":
        showTimer(response.predictResponse.fetchTime);
        break;
      default:
        throw new Error("Unknown prediction type");
    }
    updateColumnVisibility(response.prefs);
    return columns;
  }
  function main() {
    _GM_addStyle(contentCss);
    const matches = location.pathname.match(/contest\/(\d+)\/standings/);
    const contestId = matches ? matches[1] : null;
    if (contestId && document.querySelector("table.standings")) {
      predict(Number.parseInt(contestId)).then((columns) => {
      }).catch((er) => {
        console.error("[Carrot] Predict error: %o", er);
        er.toString();
      });
    }
    const ping = async () => {
      await Promise.all([maybeUpdateContestList(), maybeUpdateRatings()]);
    };
    setInterval(ping, PING_INTERVAL);
  }
  const optionsHtml = '<dialog id="options-dialog">\n  <h1>Options</h1>\n  <div id="options-content">\n    <ul>\n      <li>\n        <input type="checkbox" id="enable-predict-deltas">\n        <label for="enable-predict-deltas">\n          Predict and show deltas for running contests and recently finished contests\n        </label>\n        <ul class="inner-ul">\n          <li>\n            <details>\n              <summary>\n                TL;DR: Disable this if you are on a data capped network\n              </summary>\n              If you are on Codeforces and a contest starts in less than an hour, having this\n              option enabled will prefetch user ratings (around 7MB of data) which is required for\n              delta prediction. This is a one-time fetch for the contest. Disabling this will fetch\n              the ratings when you open the ranklist for the first time.\n            </details>\n            <input type="checkbox" id="enable-prefetch-ratings">\n            <label for="enable-prefetch-ratings">Prefetch ratings</label>\n          </li>\n        </ul>\n      </li>\n      <li>\n        <input type="checkbox" id="enable-final-deltas">\n        <label for="enable-final-deltas">\n          Show final deltas for finished rated contests\n        </label>\n      </li>\n      <button id="close-options">Close</button>\n    </ul>\n  </div>\n</dialog>';
  const optionsCss = "#options-dialog {\n  min-width: 500px;\n  min-height: 180px;\n\n  margin: 0 auto;\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 5px;\n  background-color: #f9f9f9;\n  top: 50%;\n  left: 50%;\n  -webkit-transform: translateX(-50%) translateY(-50%);\n  -moz-transform: translateX(-50%) translateY(-50%);\n  -ms-transform: translateX(-50%) translateY(-50%);\n  transform: translateX(-50%) translateY(-50%);\n}\n\n#options-dialog ul {\n  list-style-type: none;\n}\n\n#options-dialog li {\n  padding-top: 5px;\n}\n\n#options-dialog .inner-ul {\n  padding-left: 25px;\n}\n\n#options-dialog details {\n  margin-left: 10px;\n}\n\n#options-dialog details > summary {\n  margin-left: -10px;\n  cursor: pointer;\n}\n";
  async function setup() {
    const predict2 = document.querySelector("#enable-predict-deltas");
    const final = document.querySelector("#enable-final-deltas");
    const prefetch = document.querySelector("#enable-prefetch-ratings");
    async function update() {
      predict2.checked = await enablePredictDeltas();
      final.checked = await enableFinalDeltas();
      prefetch.checked = await enablePrefetchRatings();
      prefetch.disabled = !predict2.checked;
    }
    predict2.addEventListener("input", async () => {
      await enablePredictDeltas(predict2.checked);
      await update();
    });
    final.addEventListener("input", async () => {
      await enableFinalDeltas(final.checked);
      await update();
    });
    prefetch.addEventListener("input", async () => {
      await enablePrefetchRatings(prefetch.checked);
      await update();
    });
    await update();
  }
  function initOptions() {
    $("body").append(optionsHtml);
    _GM_addStyle(optionsCss);
    _GM_registerMenuCommand("Open options", () => {
      const dialog = document.querySelector("#options-dialog");
      dialog.showModal();
    });
    _GM_registerMenuCommand("Clear cache", () => {
      const list = _GM_listValues();
      for (const key of list) {
        if (key.startsWith("LOCAL.")) {
          _GM_deleteValue(key);
        }
      }
    });
    $("#close-options").on("click", () => {
      const dialog = document.querySelector("#options-dialog");
      dialog.close();
    });
    setup();
  }
  initOptions();
  main();

})();