Repository: IraSoro/peri (ts)

Issue:
Move all calculation logic to a separate module
As it was written here https://github.com/IraSoro/peri/issues/63#issuecomment-1613810939, we run into the limitation that all necessary calculations are carried out to react hooks, due to which ordinary arithmetic unit tests turn into UI tests

This can be fixed by moving all the calculations to a separate module file, and leaving the hooks as a wrapper for react

Pull request:
Separate calculation logic
Closed #74 #80 

Patch:
diff --git a/.eslintrc.json b/.eslintrc.json
index 6f86593..480bf1f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -63,7 +63,8 @@
         "src/tests/**"
       ],
       "rules": {
-        "@typescript-eslint/unbound-method": "off"
+        "@typescript-eslint/unbound-method": "off",
+        "import/first": "off"
       }
     }
   ]
diff --git a/package-lock.json b/package-lock.json
index dbefae9..5a106f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
         "eslint-plugin-react": "^7.32.2",
         "eslint-plugin-react-hooks": "^4.6.0",
         "i18next": "^23.2.11",
+        "i18next-intervalplural-postprocessor": "^3.0.0",
         "ionicons": "^7.1.2",
         "prettier": "^3.0.0",
         "react": "^18.2.0",
@@ -9839,6 +9840,11 @@
         "@babel/runtime": "^7.22.5"
       }
     },
+    "node_modules/i18next-intervalplural-postprocessor": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/i18next-intervalplural-postprocessor/-/i18next-intervalplural-postprocessor-3.0.0.tgz",
+      "integrity": "sha512-ZJWg2Gcb0kQqVI7kygLj9wVwQHK3mzRAg94uLsdoHqss0M9n0HSPr1pThd2AttkBsJFWR26G18hnKCqcTB8shQ=="
+    },
     "node_modules/iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
diff --git a/package.json b/package.json
index d405e9e..8ecc583 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "eslint-plugin-react": "^7.32.2",
     "eslint-plugin-react-hooks": "^4.6.0",
     "i18next": "^23.2.11",
+    "i18next-intervalplural-postprocessor": "^3.0.0",
     "ionicons": "^7.1.2",
     "prettier": "^3.0.0",
     "react": "^18.2.0",
diff --git a/src/i18n.ts b/src/i18n.ts
index f5e2185..a0dfd26 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -1,5 +1,6 @@
 import i18n from "i18next";
 import { initReactI18next } from "react-i18next";
+import intervalPlural from "i18next-intervalplural-postprocessor";
 
 import en from "./translations/en";
 import ru from "./translations/ru";
@@ -15,6 +16,7 @@ const resources = {
 
 i18n
   .use(initReactI18next)
+  .use(intervalPlural)
   .init({
     resources,
     lng: "en",
diff --git a/src/modals/InfoModal.tsx b/src/modals/InfoModal.tsx
index c614e30..6ddc99a 100644
--- a/src/modals/InfoModal.tsx
+++ b/src/modals/InfoModal.tsx
@@ -15,96 +15,39 @@ import {
   useDayOfCycle,
 } from "../state/CycleInformationHooks";
 
-function usePhase() {
-  const lutealPhaseLength = 14;
-  const ovulationOnError = 3;
+import { getPhase } from "../state/CalculationLogics";
 
-  const lengthOfPeriod = useLengthOfLastPeriod();
+const Phase = () => {
   const lengthOfCycle = useAverageLengthOfCycle();
-  const currentDay = Number(useDayOfCycle());
+  const lengthOfPeriod = useLengthOfLastPeriod();
+  const currentDay = useDayOfCycle();
 
   const { t } = useTranslation();
-  const phases = {
-    non: {
-      title: t("The menstrual cycle can be divided into 4 phases."),
-      description: t(
-        "When information about your cycle appears, it will be reported which phase you are in.",
-      ),
-      symptoms: [
-        t(
-          "This section will indicate the symptoms characteristic of this cycle.",
-        ),
-      ],
-    },
-    menstrual: {
-      title: t("Menstrual phase"),
-      description: t("This cycle is accompanied by low hormone levels."),
-      symptoms: [
-        t("lack of energy and strength"),
-        t("pain"),
-        t("weakness and irritability"),
-        t("increased appetite"),
-      ],
-    },
-    follicular: {
-      title: t("Follicular phase"),
-      description: t(
-        "The level of estrogen in this phase rises and reaches a maximum level.",
-      ),
-      symptoms: [
-        t("strength and vigor appear"),
-        t("endurance increases"),
-        t("new ideas and plans appear"),
-        t("libido increases"),
-      ],
-    },
-    ovulation: {
-      title: t("Ovulation phase"),
-      description: t(
-        "Once estrogen levels peak, they trigger the release of two important ovulation hormones, follicle-stimulating hormone and luteinizing hormone.",
-      ),
-      symptoms: [
-        t("increased sexual desire"),
-        t("optimistic mood"),
-        t("mild fever"),
-        t("lower abdominal pain"),
-        t("chest discomfort and bloating"),
-        t("characteristic secretions"),
-      ],
-    },
-    luteal: {
-      title: t("Luteal phase"),
-      description: t(
-        "Levels of the hormones estrogen and progesterone first rise and then drop sharply just before a period. Progesterone reaches its peak in the luteal phase.",
-      ),
-      symptoms: [
-        t("breast tenderness"),
-        t("puffiness"),
-        t("acne and skin rashes"),
-        t("increased appetite"),
-        t("diarrhea or constipation"),
-        t("irritability and depressed mood"),
-      ],
-    },
-  };
-
-  if (!lengthOfCycle || !currentDay || !lengthOfPeriod) {
-    return phases.non;
-  }
 
-  const ovulationDay = lengthOfCycle - lutealPhaseLength;
+  const phase = getPhase(lengthOfCycle, lengthOfPeriod, Number(currentDay));
 
-  if (currentDay <= lengthOfPeriod) {
-    return phases.menstrual;
-  }
-  if (currentDay <= ovulationDay - ovulationOnError) {
-    return phases.follicular;
-  }
-  if (currentDay <= ovulationDay) {
-    return phases.ovulation;
-  }
-  return phases.luteal;
-}
+  return (
+    <>
+      <div id="rectangle">
+        <IonCard>
+          <IonCardHeader class="info">{phase.title}</IonCardHeader>
+          <IonCardContent style={{ textAlign: "justify" }}>
+            {phase.description}
+          </IonCardContent>
+        </IonCard>
+      </div>
+      <div id="small-rectangle"></div>
+      <div id="rectangle">
+        <IonCard>
+          <IonCardHeader class="info">{t("Frequent symptoms")}</IonCardHeader>
+          <IonCardContent style={{ textAlign: "justify" }}>
+            <SymptomsList symptoms={phase.symptoms} />
+          </IonCardContent>
+        </IonCard>
+      </div>
+    </>
+  );
+};
 
 interface PropsSymptoms {
   symptoms: string[];
@@ -122,7 +65,6 @@ interface PropsInfoModal {
 }
 
 const InfoModal = (props: PropsInfoModal) => {
-  const phase = usePhase();
   const { t } = useTranslation();
 
   return (
@@ -143,25 +85,7 @@ const InfoModal = (props: PropsInfoModal) => {
           className="ion-padding"
           color="basic"
         >
-          <div id="rectangle">
-            <IonCard>
-              <IonCardHeader class="info">{phase.title}</IonCardHeader>
-              <IonCardContent style={{ textAlign: "justify" }}>
-                {phase.description}
-              </IonCardContent>
-            </IonCard>
-          </div>
-          <div id="small-rectangle"></div>
-          <div id="rectangle">
-            <IonCard>
-              <IonCardHeader class="info">
-                {t("Frequent symptoms")}
-              </IonCardHeader>
-              <IonCardContent style={{ textAlign: "justify" }}>
-                <SymptomsList symptoms={phase.symptoms} />
-              </IonCardContent>
-            </IonCard>
-          </div>
+          <Phase />
           <div id="small-rectangle"></div>
           <IonButton
             class="ok-modal"
diff --git a/src/pages/TabDetails.tsx b/src/pages/TabDetails.tsx
index 9251d83..0fd579f 100644
--- a/src/pages/TabDetails.tsx
+++ b/src/pages/TabDetails.tsx
@@ -33,7 +33,10 @@ function useTitleLastCycle() {
     return "";
   }
 
-  return `${dayOfCycle} ${t("Day")}`;
+  return `${dayOfCycle} ${t("Days_interval", {
+    postProcess: "interval",
+    count: 1, // NOTE: to indicate which day is in the account, you need to write the day as if in the singular
+  })}`;
 }
 
 function useProgressBarBuffer() {
@@ -67,17 +70,11 @@ export function useInfoForOneCycle(idx: number): InfoOneCycle {
       dates: t("date"),
     };
   }
-  const cycleLenNumber: number = cycles[idx].cycleLength;
-  let cycleLenString: string;
-  if (
-    cycleLenNumber > 20 &&
-    cycleLenNumber % 10 > 0 &&
-    cycleLenNumber % 10 < 5
-  ) {
-    cycleLenString = `${cycleLenNumber} ${t("Days less 5")}`;
-  } else {
-    cycleLenString = `${cycleLenNumber} ${t("Days")}`;
-  }
+  const cycleLenNumber = cycles[idx].cycleLength;
+  const cycleLenString = `${cycleLenNumber} ${t("Days_interval", {
+    postProcess: "interval",
+    count: cycleLenNumber,
+  })}`;
 
   const periodLenNumber: number = cycles[idx].periodLength;
 
@@ -179,28 +176,20 @@ const TabDetails = () => {
 
   const [confirmAlert] = useIonAlert();
 
+  const updateCycles = useContext(CyclesContext).updateCycles;
+
   const averageLengthOfCycle = useAverageLengthOfCycle();
   const averageLengthOfPeriod = useAverageLengthOfPeriod();
-  let lengthOfCycle: string;
-  let lengthOfPeriod: string;
-
-  if (
-    averageLengthOfCycle >= 20 &&
-    averageLengthOfCycle % 10 < 5 &&
-    averageLengthOfCycle % 10 > 0
-  ) {
-    lengthOfCycle = `${averageLengthOfCycle} ${t("Days less 5")}`;
-  } else {
-    lengthOfCycle = `${averageLengthOfCycle} ${t("Days")}`;
-  }
 
-  if (averageLengthOfPeriod < 5 && averageLengthOfPeriod > 0) {
-    lengthOfPeriod = `${averageLengthOfPeriod} ${t("Days less 5")}`;
-  } else {
-    lengthOfPeriod = `${averageLengthOfPeriod} ${t("Days")}`;
-  }
+  const lengthOfCycle = `${averageLengthOfCycle} ${t("Days_interval", {
+    postProcess: "interval",
+    count: averageLengthOfCycle,
+  })}`;
 
-  const updateCycles = useContext(CyclesContext).updateCycles;
+  const lengthOfPeriod = `${averageLengthOfPeriod} ${t("Days_interval", {
+    postProcess: "interval",
+    count: averageLengthOfPeriod,
+  })}`;
 
   const p_style = {
     fontSize: "10px" as const,
diff --git a/src/pages/TabHome.tsx b/src/pages/TabHome.tsx
index cd4776a..3dd6084 100644
--- a/src/pages/TabHome.tsx
+++ b/src/pages/TabHome.tsx
@@ -30,123 +30,86 @@ import MarkModal from "../modals/MarkModal";
 import InfoModal from "../modals/InfoModal";
 import CalendarModal from "../modals/CalendarModal";
 
-const millisecondsInDay = 24 * 60 * 60 * 1000;
+import {
+  getOvulationStatus,
+  getPregnancyChance,
+  getDaysBeforePeriod,
+} from "../state/CalculationLogics";
+
+const pStyle = {
+  fontSize: "10px" as const,
+  color: "var(--ion-color-light)" as const,
+};
+
+const hStyle = {
+  fontWeight: "bold" as const,
+  color: "var(--ion-color-light)" as const,
+};
 
-function useOvulationStatus(): string {
+function DaysBeforePeriod() {
+  const startDate = useLastStartDate();
   const cycleLength = useAverageLengthOfCycle();
-  const dayOfCycle = Number(useDayOfCycle());
-  const { t } = useTranslation();
 
-  if (!cycleLength || !dayOfCycle) {
-    return "";
-  }
+  const daysBeforePeriod = getDaysBeforePeriod(cycleLength, startDate);
 
-  const lutealPhaseLength = 14;
-  const ovulationDay = Number(cycleLength) - lutealPhaseLength;
-  const diffDay = ovulationDay - dayOfCycle;
-  if (diffDay === 0) {
-    return t("today");
-  } else if (diffDay < 0 && diffDay >= -2) {
-    return t("possible");
-  } else if (diffDay < 0) {
-    return t("finished");
-  } else if (diffDay === 1) {
-    return t("tomorrow");
-  } else if (diffDay < 5) {
-    return `${t("in")} ${diffDay} ${t("Days less 5")}`;
-  }
-  return `${t("in")} ${diffDay} ${t("Days")}`;
+  return (
+    <>
+      <IonLabel style={{ textAlign: "center" }}>
+        <h2>{daysBeforePeriod.title}</h2>
+      </IonLabel>
+      <IonLabel
+        style={{ textAlign: "center" }}
+        color="dark-basic"
+      >
+        <h1 style={{ fontWeight: "bold" }}>{daysBeforePeriod.days}</h1>
+      </IonLabel>
+    </>
+  );
 }
 
-function usePregnancyChance() {
-  const cycleLength = useAverageLengthOfCycle();
-  const dayOfCycle = Number(useDayOfCycle());
+const DayOfCycle = () => {
+  const dayOfCycle = useDayOfCycle();
   const { t } = useTranslation();
 
-  if (!cycleLength || !dayOfCycle) {
-    return "";
-  }
+  return (
+    <>
+      <p style={pStyle}>{t("Current cycle day")}</p>
+      <h1 style={hStyle}>{dayOfCycle}</h1>
+    </>
+  );
+};
 
-  const lutealPhaseLength = 14;
-  const ovulationDay = Number(cycleLength) - lutealPhaseLength;
-  const diffDay = ovulationDay - dayOfCycle;
+const OvulationStatus = () => {
+  const cycleLength = useAverageLengthOfCycle();
+  const dayOfCycle = Number(useDayOfCycle());
 
-  if (diffDay <= 4 && diffDay >= -2) {
-    return t("high");
-  }
-  return t("low");
-}
+  const { t } = useTranslation();
 
-interface DaysBeforePeriod {
-  title: string;
-  days: string;
-}
+  const ovulationStatus = getOvulationStatus(cycleLength, dayOfCycle);
 
-function useDaysBeforePeriod(): DaysBeforePeriod {
-  const startDate = useLastStartDate();
+  return (
+    <>
+      <p style={pStyle}>{t("Ovulation")}</p>
+      <h1 style={hStyle}>{ovulationStatus}</h1>
+    </>
+  );
+};
+
+const PregnancyChance = () => {
   const cycleLength = useAverageLengthOfCycle();
+  const dayOfCycle = Number(useDayOfCycle());
+
   const { t } = useTranslation();
 
-  if (!startDate || !cycleLength) {
-    return {
-      title: t("Period in"),
-      days: t("no info"),
-    };
-  }
+  const pregnancyChance = getPregnancyChance(cycleLength, dayOfCycle);
 
-  const dateOfFinish = new Date(startDate);
-  dateOfFinish.setDate(dateOfFinish.getDate() + Number(cycleLength));
-  dateOfFinish.setHours(0, 0, 0, 0);
-  const now = new Date();
-  now.setHours(0, 0, 0, 0);
-  const dayBefore = Math.round(
-    (Number(dateOfFinish) - Number(now)) / millisecondsInDay,
+  return (
+    <>
+      <p style={pStyle}>{t("Chance of getting pregnant")}</p>
+      <h1 style={hStyle}>{pregnancyChance}</h1>
+    </>
   );
-
-  if (dayBefore > 1) {
-    if (
-      dayBefore < 5 ||
-      (dayBefore > 20 && dayBefore % 10 > 0 && dayBefore % 10 < 5)
-    ) {
-      return {
-        title: t("Period in"),
-        days: `${dayBefore} ${t("Days less 5")}`,
-      };
-    }
-    return {
-      title: t("Period in"),
-      days: `${dayBefore} ${t("Days")}`,
-    };
-  }
-  if (dayBefore === 1) {
-    return {
-      title: t("Period in"),
-      days: `1 ${t("Days")}`,
-    };
-  }
-  if (dayBefore === 0) {
-    return {
-      title: t("Period"),
-      days: t("today"),
-    };
-  }
-  if (dayBefore === -1) {
-    return {
-      title: t("Delay"),
-      days: `1 ${t("Day")}`,
-    };
-  }
-  if (dayBefore > -5) {
-    return {
-      title: t("Delay"),
-      days: `${dayBefore} ${t("Days less 5")}`,
-    };
-  }
-  return {
-    title: t("Delay"),
-    days: `${Math.abs(dayBefore)} ${t("Days")}`,
-  };
-}
+};
 
 interface HomeProps {
   isLanguageModal: boolean;
@@ -161,13 +124,6 @@ const TabHome = (props: HomeProps) => {
 
   const router = useIonRouter();
 
-  const dayOfCycle = useDayOfCycle();
-  const ovulationStatus = useOvulationStatus();
-  const pregnancyChance = usePregnancyChance();
-  const daysBeforePeriod = useDaysBeforePeriod();
-
-  const { t } = useTranslation();
-
   useEffect(() => {
     storage.get.cycles().catch((err) => {
       console.error(`Can't get cycles ${(err as Error).message}`);
@@ -203,16 +159,6 @@ const TabHome = (props: HomeProps) => {
     };
   }, [router, isInfoModal, isCalendarModal, isMarkModal, props]);
 
-  const p_style = {
-    fontSize: "10px" as const,
-    color: "var(--ion-color-light)" as const,
-  };
-
-  const h_style = {
-    fontWeight: "bold" as const,
-    color: "var(--ion-color-light)" as const,
-  };
-
   return (
     <IonPage>
       <IonContent
@@ -244,17 +190,7 @@ const TabHome = (props: HomeProps) => {
               </IonCol>
               <IonCol>
                 <div>
-                  <IonLabel style={{ textAlign: "center" }}>
-                    <h2>{daysBeforePeriod.title}</h2>
-                  </IonLabel>
-                  <IonLabel
-                    style={{ textAlign: "center" }}
-                    color="dark-basic"
-                  >
-                    <h1 style={{ fontWeight: "bold" }}>
-                      {daysBeforePeriod.days}
-                    </h1>
-                  </IonLabel>
+                  <DaysBeforePeriod />
                   <MarkModal
                     isOpen={isMarkModal}
                     setIsOpen={setIsMarkModal}
@@ -269,8 +205,7 @@ const TabHome = (props: HomeProps) => {
                   lines="full"
                 >
                   <IonLabel>
-                    <p style={p_style}>{t("Current cycle day")}</p>
-                    <h1 style={h_style}>{dayOfCycle}</h1>
+                    <DayOfCycle />
                   </IonLabel>
                 </IonItem>
                 <IonItem
@@ -278,8 +213,7 @@ const TabHome = (props: HomeProps) => {
                   lines="full"
                 >
                   <IonLabel>
-                    <p style={p_style}>{t("Ovulation")}</p>
-                    <h1 style={h_style}>{ovulationStatus}</h1>
+                    <OvulationStatus />
                   </IonLabel>
                 </IonItem>
                 <IonItem
@@ -287,8 +221,7 @@ const TabHome = (props: HomeProps) => {
                   lines="none"
                 >
                   <IonLabel>
-                    <p style={p_style}>{t("Chance of getting pregnant")}</p>
-                    <h1 style={h_style}>{pregnancyChance}</h1>
+                    <PregnancyChance />
                   </IonLabel>
                 </IonItem>
               </IonCardContent>
diff --git a/src/state/CalculationLogics.ts b/src/state/CalculationLogics.ts
new file mode 100644
index 0000000..6749da8
--- /dev/null
+++ b/src/state/CalculationLogics.ts
@@ -0,0 +1,231 @@
+import i18n from "i18next";
+import { Cycle } from "../data/ClassCycle";
+
+const millisecondsInDay = 24 * 60 * 60 * 1000;
+
+export function getOvulationStatus(cycleLength: number, dayOfCycle: number) {
+  if (!cycleLength || !dayOfCycle) {
+    return "";
+  }
+
+  const lutealPhaseLength = 14;
+  const ovulationDay = cycleLength - lutealPhaseLength;
+  const diffDay = ovulationDay - dayOfCycle;
+  if (diffDay < -2) {
+    return i18n.t("finished");
+  }
+  if (diffDay < 0) {
+    return i18n.t("possible");
+  }
+  if (diffDay === 0) {
+    return i18n.t("today");
+  }
+  if (diffDay === 1) {
+    return i18n.t("tomorrow");
+  }
+  return `${i18n.t("in")} ${diffDay} ${i18n.t("Days_interval", {
+    postProcess: "interval",
+    count: diffDay,
+  })}`;
+}
+
+export function getPregnancyChance(cycleLength: number, dayOfCycle: number) {
+  if (!cycleLength || !dayOfCycle) {
+    return "";
+  }
+
+  const lutealPhaseLength = 14;
+  const ovulationDay = cycleLength - lutealPhaseLength;
+  const diffDay = ovulationDay - dayOfCycle;
+
+  if (diffDay >= -2 && diffDay <= 4) {
+    return i18n.t("high");
+  }
+  return i18n.t("low");
+}
+
+export function getDaysBeforePeriod(cycleLength: number, startDate: string) {
+  if (!startDate || !cycleLength) {
+    return {
+      title: i18n.t("Period in"),
+      days: i18n.t("no info"),
+    };
+  }
+
+  const dateOfFinish = new Date(startDate);
+  dateOfFinish.setDate(dateOfFinish.getDate() + cycleLength);
+  dateOfFinish.setHours(0, 0, 0, 0);
+  const now = new Date();
+  now.setHours(0, 0, 0, 0);
+  const dayBefore = Math.round(
+    (Number(dateOfFinish) - Number(now)) / millisecondsInDay,
+  );
+
+  if (dayBefore > 0) {
+    return {
+      title: i18n.t("Period in"),
+      days: `${dayBefore} ${i18n.t("Days_interval", {
+        postProcess: "interval",
+        count: dayBefore,
+      })}`,
+    };
+  }
+  if (dayBefore === 0) {
+    return {
+      title: i18n.t("Period"),
+      days: i18n.t("today"),
+    };
+  }
+  return {
+    title: i18n.t("Delay"),
+    days: `${Math.abs(dayBefore)} ${i18n.t("Days_interval", {
+      postProcess: "interval",
+      count: Math.abs(dayBefore),
+    })}`,
+  };
+}
+
+export function getDayOfCycle(startDate: string) {
+  if (!startDate) {
+    return "";
+  }
+
+  const start = new Date(startDate);
+  start.setHours(0, 0, 0, 0);
+  const currentDate = new Date();
+  currentDate.setHours(0, 0, 0, 0);
+
+  const diff =
+    Math.ceil((currentDate.getTime() - start.getTime()) / millisecondsInDay) +
+    1;
+
+  return diff.toString();
+}
+
+export function getPhase(
+  lengthOfCycle: number,
+  lengthOfPeriod: number,
+  currentDay: number,
+) {
+  const lutealPhaseLength = 14;
+  const ovulationOnError = 3;
+
+  const phases = {
+    non: {
+      title: i18n.t("The menstrual cycle can be divided into 4 phases."),
+      description: i18n.t(
+        "When information about your cycle appears, it will be reported which phase you are in.",
+      ),
+      symptoms: [
+        i18n.t(
+          "This section will indicate the symptoms characteristic of this cycle.",
+        ),
+      ],
+    },
+    menstrual: {
+      title: i18n.t("Menstrual phase"),
+      description: i18n.t("This cycle is accompanied by low hormone levels."),
+      symptoms: [
+        i18n.t("lack of energy and strength"),
+        i18n.t("pain"),
+        i18n.t("weakness and irritability"),
+        i18n.t("increased appetite"),
+      ],
+    },
+    follicular: {
+      title: i18n.t("Follicular phase"),
+      description: i18n.t(
+        "The level of estrogen in this phase rises and reaches a maximum level.",
+      ),
+      symptoms: [
+        i18n.t("strength and vigor appear"),
+        i18n.t("endurance increases"),
+        i18n.t("new ideas and plans appear"),
+        i18n.t("libido increases"),
+      ],
+    },
+    ovulation: {
+      title: i18n.t("Ovulation phase"),
+      description: i18n.t(
+        "Once estrogen levels peak, they trigger the release of two important ovulation hormones, follicle-stimulating hormone and luteinizing hormone.",
+      ),
+      symptoms: [
+        i18n.t("increased sexual desire"),
+        i18n.t("optimistic mood"),
+        i18n.t("mild fever"),
+        i18n.t("lower abdominal pain"),
+        i18n.t("chest discomfort and bloating"),
+        i18n.t("characteristic secretions"),
+      ],
+    },
+    luteal: {
+      title: i18n.t("Luteal phase"),
+      description: i18n.t(
+        "Levels of the hormones estrogen and progesterone first rise and then drop sharply just before a period. Progesterone reaches its peak in the luteal phase.",
+      ),
+      symptoms: [
+        i18n.t("breast tenderness"),
+        i18n.t("puffiness"),
+        i18n.t("acne and skin rashes"),
+        i18n.t("increased appetite"),
+        i18n.t("diarrhea or constipation"),
+        i18n.t("irritability and depressed mood"),
+      ],
+    },
+  };
+
+  if (!lengthOfCycle || !currentDay || !lengthOfPeriod) {
+    return phases.non;
+  }
+
+  const ovulationDay = lengthOfCycle - lutealPhaseLength;
+
+  if (currentDay <= lengthOfPeriod) {
+    return phases.menstrual;
+  }
+  if (currentDay <= ovulationDay - ovulationOnError) {
+    return phases.follicular;
+  }
+  if (currentDay <= ovulationDay) {
+    return phases.ovulation;
+  }
+  return phases.luteal;
+}
+
+export function getAverageLengthOfCycle(cycles: Cycle[]) {
+  if (cycles.length === 0) {
+    return 0;
+  }
+
+  if (cycles.length === 1) {
+    return cycles[0].cycleLength;
+  }
+
+  const sum = cycles.reduce((prev, current, idx) => {
+    if (idx > 0) {
+      return prev + current.cycleLength;
+    }
+    return 0;
+  }, 0);
+
+  return Math.round(sum / (cycles.length - 1));
+}
+
+export function getAverageLengthOfPeriod(cycles: Cycle[]) {
+  if (cycles.length === 0) {
+    return 0;
+  }
+
+  if (cycles.length === 1) {
+    return cycles[0].periodLength;
+  }
+
+  const sum = cycles.reduce((prev, current, idx) => {
+    if (idx > 0) {
+      return prev + current.periodLength;
+    }
+    return 0;
+  }, 0);
+
+  return Math.round(sum / (cycles.length - 1));
+}
diff --git a/src/state/CycleInformationHooks.tsx b/src/state/CycleInformationHooks.tsx
index 69e2037..b7ae5b2 100644
--- a/src/state/CycleInformationHooks.tsx
+++ b/src/state/CycleInformationHooks.tsx
@@ -1,7 +1,10 @@
 import { useContext } from "react";
 import { CyclesContext } from "./Context";
-
-const millisecondsInDay = 24 * 60 * 60 * 1000;
+import {
+  getDayOfCycle,
+  getAverageLengthOfCycle,
+  getAverageLengthOfPeriod,
+} from "./CalculationLogics";
 
 export function useLastStartDate(): string {
   const cycles = useContext(CyclesContext).cycles;
@@ -15,63 +18,17 @@ export function useLastStartDate(): string {
 
 export function useDayOfCycle(): string {
   const startDate = useLastStartDate();
-
-  if (!startDate) {
-    return "";
-  }
-
-  const start = new Date(startDate);
-  start.setHours(0, 0, 0, 0);
-  const currentDate = new Date();
-  currentDate.setHours(0, 0, 0, 0);
-
-  const diff =
-    Math.ceil((currentDate.getTime() - start.getTime()) / millisecondsInDay) +
-    1;
-
-  return diff.toString();
+  return getDayOfCycle(startDate);
 }
 
 export function useAverageLengthOfCycle(): number {
   const cycles = useContext(CyclesContext).cycles;
-
-  if (cycles.length === 0) {
-    return 0;
-  }
-
-  if (cycles.length === 1) {
-    return cycles[0].cycleLength;
-  }
-
-  const sum = cycles.reduce((prev, current, idx) => {
-    if (idx > 0) {
-      return prev + current.cycleLength;
-    }
-    return 0;
-  }, 0);
-
-  return Math.round(sum / (cycles.length - 1));
+  return getAverageLengthOfCycle(cycles);
 }
 
 export function useAverageLengthOfPeriod(): number {
   const cycles = useContext(CyclesContext).cycles;
-
-  if (cycles.length === 0) {
-    return 0;
-  }
-
-  if (cycles.length === 1) {
-    return cycles[0].periodLength;
-  }
-
-  const sum = cycles.reduce((prev, current, idx) => {
-    if (idx > 0) {
-      return prev + current.periodLength;
-    }
-    return 0;
-  }, 0);
-
-  return Math.round(sum / (cycles.length - 1));
+  return getAverageLengthOfPeriod(cycles);
 }
 
 export function useLengthOfLastPeriod(): number {
diff --git a/src/translations/en.tsx b/src/translations/en.tsx
index 03d00a8..55f34bc 100644
--- a/src/translations/en.tsx
+++ b/src/translations/en.tsx
@@ -8,9 +8,7 @@ const en = {
   Period: "Period",
   today: "today",
   Delay: "Delay",
-  Day: "Day",
-  Days: "Days",
-  "Days less 5": "Days", // for russian language
+  Days_interval: "(0,2-inf){Days};(1){Day};",
   "Current cycle day": "Current cycle day",
   Ovulation: "Ovulation",
   possible: "possible",
diff --git a/src/translations/ru.tsx b/src/translations/ru.tsx
index c89a1e2..00f4cfc 100644
--- a/src/translations/ru.tsx
+++ b/src/translations/ru.tsx
@@ -8,9 +8,8 @@ const ru = {
   Period: "Месячные",
   today: "сегодня",
   Delay: "Задержка",
-  Day: "День",
-  Days: "Дней",
-  "Days less 5": "Дня", // for russian language
+  Days_interval:
+    "(0)[Дней];(1)[День];(2-4)[Дня];(5-20)[Дней];(21)[День];(22-24)[Дня];(25-30)[Дней];(31)[День];(32-34)[Дня];(35-40)[Дней];",
   "Current cycle day": "Текущий день цикла",
   Ovulation: "Овуляция",
   possible: "может быть сегодня",


Test patch:
diff --git a/src/tests/CalculationLogic.test.ts b/src/tests/CalculationLogic.test.ts
new file mode 100644
index 0000000..ab760c2
--- /dev/null
+++ b/src/tests/CalculationLogic.test.ts
@@ -0,0 +1,353 @@
+import i18n from "i18next";
+import { Cycle } from "../data/ClassCycle";
+import {
+  getOvulationStatus,
+  getPregnancyChance,
+  getDayOfCycle,
+  getAverageLengthOfCycle,
+  getAverageLengthOfPeriod,
+  getDaysBeforePeriod,
+  getPhase,
+} from "../state/CalculationLogics";
+
+describe("getOvulationStatus", () => {
+  test("cycles array is empty", () => {
+    expect(getOvulationStatus(0, 0)).toEqual("");
+  });
+
+  test("a few days before ovulation", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getOvulationStatus(28, 5)).toEqual(
+      `${i18n.t("in")} 9 ${i18n.t("Days_interval", {
+        postProcess: "interval",
+        count: 9,
+      })}`,
+    );
+  });
+
+  test("ovulation is tomorrow", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementationOnce((key) => key);
+    expect(getOvulationStatus(28, 13)).toEqual("tomorrow");
+  });
+
+  test("ovulation is today", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementationOnce((key) => key);
+    expect(getOvulationStatus(28, 14)).toEqual("today");
+  });
+
+  test("if ovulation is possible", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementationOnce((key) => key);
+    expect(getOvulationStatus(28, 15)).toEqual("possible");
+  });
+
+  test("ovulation is finished", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementationOnce((key) => key);
+    expect(getOvulationStatus(28, 18)).toEqual("finished");
+  });
+});
+
+describe("getPregnancyChance", () => {
+  test("cycles array is empty", () => {
+    expect(getPregnancyChance(0, 0)).toEqual("");
+  });
+
+  test("pregnancy chance is high", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPregnancyChance(28, 14)).toEqual("high");
+  });
+
+  test("pregnancy chance is low", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPregnancyChance(28, 20)).toEqual("low");
+  });
+});
+
+describe("getDayOfCycle", () => {
+  test("no info about start cycle date", () => {
+    expect(getDayOfCycle("")).toEqual("");
+  });
+
+  test("middle of the cycle", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const date: Date = new Date();
+    date.setHours(0, 0, 0, 0);
+    date.setDate(date.getDate() - 13);
+    expect(getDayOfCycle(date.toString())).toEqual("14");
+  });
+});
+
+describe("getAverageLengthOfCycle", () => {
+  test("cycles array is empty", () => {
+    expect(getAverageLengthOfCycle([])).toEqual(0);
+  });
+
+  test("only one item in the cycles array", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const cycles: Cycle[] = [
+      {
+        cycleLength: 28,
+        periodLength: 5,
+        startDate: "2023-06-30",
+      },
+    ];
+    expect(getAverageLengthOfCycle(cycles)).toEqual(28);
+  });
+
+  test("more than one item in the cycles array", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const cycles: Cycle[] = [
+      {
+        cycleLength: 0,
+        periodLength: 6,
+        startDate: "2023-06-30",
+      },
+      {
+        cycleLength: 29,
+        periodLength: 6,
+        startDate: "2023-06-03",
+      },
+      {
+        cycleLength: 27,
+        periodLength: 4,
+        startDate: "2023-05-07",
+      },
+    ];
+    expect(getAverageLengthOfCycle(cycles)).toEqual(28);
+  });
+});
+
+describe("getAverageLengthOfPeriod", () => {
+  test("cycles array is empty", () => {
+    expect(getAverageLengthOfPeriod([])).toEqual(0);
+  });
+
+  test("only one item in the cycles array", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const cycles: Cycle[] = [
+      {
+        cycleLength: 28,
+        periodLength: 5,
+        startDate: "2023-06-30",
+      },
+    ];
+    expect(getAverageLengthOfPeriod(cycles)).toEqual(5);
+  });
+
+  test("more than one item in the cycles array", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const cycles: Cycle[] = [
+      {
+        cycleLength: 0,
+        periodLength: 6,
+        startDate: "2023-06-30",
+      },
+      {
+        cycleLength: 28,
+        periodLength: 6,
+        startDate: "2023-06-03",
+      },
+      {
+        cycleLength: 26,
+        periodLength: 4,
+        startDate: "2023-05-07",
+      },
+    ];
+    expect(getAverageLengthOfPeriod(cycles)).toEqual(5);
+  });
+});
+
+describe("getDaysBeforePeriod", () => {
+  test("cycles array is empty", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    expect(getDaysBeforePeriod(0, "")).toEqual({
+      title: i18n.t("Period in"),
+      days: i18n.t("no info"),
+    });
+  });
+
+  test("period in the few days", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const date: Date = new Date();
+    date.setHours(0, 0, 0, 0);
+    date.setDate(date.getDate() - 18);
+    expect(getDaysBeforePeriod(28, date.toString())).toEqual({
+      title: i18n.t("Period in"),
+      days: `10 ${i18n.t("Days_interval", {
+        postProcess: "interval",
+        count: 10,
+      })}`,
+    });
+  });
+
+  test("period in 1 Day", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const date: Date = new Date();
+    date.setHours(0, 0, 0, 0);
+    date.setDate(date.getDate() - 27);
+    expect(getDaysBeforePeriod(28, date.toString())).toEqual({
+      title: i18n.t("Period in"),
+      days: `1 ${i18n.t("Days_interval", {
+        postProcess: "interval",
+        count: 1,
+      })}`,
+    });
+  });
+
+  test("period is today", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const date: Date = new Date();
+    date.setHours(0, 0, 0, 0);
+    date.setDate(date.getDate() - 28);
+    expect(getDaysBeforePeriod(28, date.toString())).toEqual({
+      title: i18n.t("Period"),
+      days: i18n.t("today"),
+    });
+  });
+
+  test("delay 1 day", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const date: Date = new Date();
+    date.setHours(0, 0, 0, 0);
+    date.setDate(date.getDate() - 29);
+    expect(getDaysBeforePeriod(28, date.toString())).toEqual({
+      title: i18n.t("Delay"),
+      days: `1 ${i18n.t("Days_interval", {
+        postProcess: "interval",
+        count: 1,
+      })}`,
+    });
+  });
+
+  test("delay a few days", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+
+    const date: Date = new Date();
+    date.setHours(0, 0, 0, 0);
+    date.setDate(date.getDate() - 38);
+    expect(getDaysBeforePeriod(28, date.toString())).toEqual({
+      title: i18n.t("Delay"),
+      days: `10 ${i18n.t("Days_interval", {
+        postProcess: "interval",
+        count: 10,
+      })}`,
+    });
+  });
+});
+
+const phases = {
+  non: {
+    title: "The menstrual cycle can be divided into 4 phases.",
+    description:
+      "When information about your cycle appears, it will be reported which phase you are in.",
+    symptoms: [
+      "This section will indicate the symptoms characteristic of this cycle.",
+    ],
+  },
+  menstrual: {
+    title: "Menstrual phase",
+    description: "This cycle is accompanied by low hormone levels.",
+    symptoms: [
+      "lack of energy and strength",
+      "pain",
+      "weakness and irritability",
+      "increased appetite",
+    ],
+  },
+  follicular: {
+    title: "Follicular phase",
+    description:
+      "The level of estrogen in this phase rises and reaches a maximum level.",
+    symptoms: [
+      "strength and vigor appear",
+      "endurance increases",
+      "new ideas and plans appear",
+      "libido increases",
+    ],
+  },
+  ovulation: {
+    title: "Ovulation phase",
+    description:
+      "Once estrogen levels peak, they trigger the release of two important ovulation hormones, follicle-stimulating hormone and luteinizing hormone.",
+    symptoms: [
+      "increased sexual desire",
+      "optimistic mood",
+      "mild fever",
+      "lower abdominal pain",
+      "chest discomfort and bloating",
+      "characteristic secretions",
+    ],
+  },
+  luteal: {
+    title: "Luteal phase",
+    description:
+      "Levels of the hormones estrogen and progesterone first rise and then drop sharply just before a period. Progesterone reaches its peak in the luteal phase.",
+    symptoms: [
+      "breast tenderness",
+      "puffiness",
+      "acne and skin rashes",
+      "increased appetite",
+      "diarrhea or constipation",
+      "irritability and depressed mood",
+    ],
+  },
+};
+
+describe("getPhase", () => {
+  test("cycles array is empty", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPhase(0, 0, 0)).toEqual(phases.non);
+  });
+
+  test("cycle phase is menstrual", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPhase(28, 5, 1)).toEqual(phases.menstrual);
+  });
+
+  test("cycle phase is follicular", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPhase(28, 5, 7)).toEqual(phases.follicular);
+  });
+
+  test("cycle phase is ovulation", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPhase(28, 5, 14)).toEqual(phases.ovulation);
+  });
+
+  test("cycle phase is luteal", () => {
+    // @ts-expect-error mocked `t` method
+    jest.spyOn(i18n, "t").mockImplementation((key) => key);
+    expect(getPhase(28, 5, 21)).toEqual(phases.luteal);
+  });
+});