diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b90d68d..1114f1e 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,40 +1 @@
-# CODEOWNERS
-#
-# Define quién debe revisar cambios en partes específicas del repositorio.
-# GitHub solicita automáticamente la revisión de los owners cuando un PR
-# modifica archivos que coinciden con algún patrón.
-#
-# Sintaxis: patrón @usuario-o-equipo
-#
-# Las reglas se evalúan de arriba hacia abajo.
-# La última regla que coincida con un archivo gana.
-# Los patrones siguen la misma sintaxis que .gitignore.
-
-# --- Owner global ---
-# Aplica a todo lo que no tenga una regla más específica abajo.
-* @TU_USUARIO
-
-# --- Librería principal ---
-# Cualquier cambio en el código fuente requiere revisión de los maintainers.
-/src/ @TU_USUARIO
-
-# --- Tests ---
-# Los tests pueden revisarlos los mismos developers, no solo maintainers.
-# En un equipo real aquí irían @equipo/developers o usuarios individuales.
-/tests/ @TU_USUARIO
-
-# --- Workflows de CI/CD ---
-# Los cambios en pipelines tienen un impacto alto: pueden exponer secrets,
-# modificar qué checks son requeridos, o cambiar tiempos de build.
-# Requieren revisión explícita de quien administra la infraestructura.
-/.github/workflows/ @TU_USUARIO
-
-# --- Configuración de seguridad y políticas ---
-# CODEOWNERS mismo, editorconfig y configuración del repo.
-/.github/CODEOWNERS @TU_USUARIO
-/.editorconfig @TU_USUARIO
-
-# --- Documentación ---
-# El README puede actualizarlo cualquier contributor.
-# Si quisieras restricción, agregarías un owner específico aquí.
-*.md @TU_USUARIO
+* @leopansa
\ No newline at end of file
diff --git a/.github/code_OWNERS_back.aa b/.github/code_OWNERS_back.aa
new file mode 100644
index 0000000..b90d68d
--- /dev/null
+++ b/.github/code_OWNERS_back.aa
@@ -0,0 +1,40 @@
+# CODEOWNERS
+#
+# Define quién debe revisar cambios en partes específicas del repositorio.
+# GitHub solicita automáticamente la revisión de los owners cuando un PR
+# modifica archivos que coinciden con algún patrón.
+#
+# Sintaxis: patrón @usuario-o-equipo
+#
+# Las reglas se evalúan de arriba hacia abajo.
+# La última regla que coincida con un archivo gana.
+# Los patrones siguen la misma sintaxis que .gitignore.
+
+# --- Owner global ---
+# Aplica a todo lo que no tenga una regla más específica abajo.
+* @TU_USUARIO
+
+# --- Librería principal ---
+# Cualquier cambio en el código fuente requiere revisión de los maintainers.
+/src/ @TU_USUARIO
+
+# --- Tests ---
+# Los tests pueden revisarlos los mismos developers, no solo maintainers.
+# En un equipo real aquí irían @equipo/developers o usuarios individuales.
+/tests/ @TU_USUARIO
+
+# --- Workflows de CI/CD ---
+# Los cambios en pipelines tienen un impacto alto: pueden exponer secrets,
+# modificar qué checks son requeridos, o cambiar tiempos de build.
+# Requieren revisión explícita de quien administra la infraestructura.
+/.github/workflows/ @TU_USUARIO
+
+# --- Configuración de seguridad y políticas ---
+# CODEOWNERS mismo, editorconfig y configuración del repo.
+/.github/CODEOWNERS @TU_USUARIO
+/.editorconfig @TU_USUARIO
+
+# --- Documentación ---
+# El README puede actualizarlo cualquier contributor.
+# Si quisieras restricción, agregarías un owner específico aquí.
+*.md @TU_USUARIO
diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml
new file mode 100644
index 0000000..a713bfd
--- /dev/null
+++ b/.github/workflows/01-ci.yml
@@ -0,0 +1,123 @@
+# Estado esperado de .github/workflows/01-ci.yml al terminar el laboratorio 04.
+# Cambios respecto al 03: el trabajo se parte en dos jobs unidos con `needs`,
+# y los binarios viajan de un job a otro como artefacto.
+#
+# Ojo con el costo: dos jobs significan dos runners, dos checkouts y dos
+# restores. Partir jobs se justifica cuando ganas paralelismo o cuando
+# necesitas condiciones distintas, no por estética.
+
+name: 01 - CI
+
+on:
+ push:
+ branches:
+ - main
+ - 'lab/**'
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ DOTNET_VERSION: '9.0.x'
+ DOTNET_NOLOGO: 'true'
+ DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ outputs:
+ version-sdk: ${{ steps.sdk.outputs.dotnet-version }}
+ steps:
+ - name: Descargar el código al runner
+ uses: actions/checkout@v7
+
+ - name: Instalar el SDK de .NET
+ id: sdk
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Restaurar dependencias
+ run: dotnet restore
+
+ - name: Compilar en Release
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Publicar los binarios compilados
+ uses: actions/upload-artifact@v7
+ with:
+ name: binarios
+ path: |
+ src/**/bin/Release/
+ tests/**/bin/Release/
+ retention-days: 1
+ if-no-files-found: error
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Descargar el código al runner
+ uses: actions/checkout@v7
+
+ - name: Instalar el SDK de .NET
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Bajar los binarios del job anterior
+ uses: actions/download-artifact@v7
+ with:
+ name: binarios
+
+ # El artefacto trae bin/, pero no obj/. `dotnet test --no-build` necesita
+ # el archivo obj/project.assets.json para resolver rutas de salida, así que
+ # hay que restaurar de nuevo. Es rápido porque no vuelve a compilar.
+ - name: Restaurar dependencias
+ run: dotnet restore
+
+ - name: Ejecutar pruebas
+ run: |
+ dotnet test \
+ --configuration Release \
+ --no-build \
+ --logger "trx;LogFileName=resultados.trx" \
+ --results-directory ./resultados
+
+ - name: Publicar resultados de pruebas
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: resultados-de-pruebas
+ path: ./resultados
+ retention-days: 5
+ if-no-files-found: error
+
+ - name: Escribir el resumen del run
+ if: always()
+ run: |
+ TRX=$(find ./resultados -name '*.trx' | head -1)
+ TOTAL=$(grep -o 'total="[0-9]*"' "$TRX" | head -1 | grep -o '[0-9]*')
+ PASADAS=$(grep -o 'passed="[0-9]*"' "$TRX" | head -1 | grep -o '[0-9]*')
+ FALLIDAS=$(grep -o 'failed="[0-9]*"' "$TRX" | head -1 | grep -o '[0-9]*')
+ {
+ echo "### Resultado de las pruebas"
+ echo ""
+ echo "| Métrica | Valor |"
+ echo "|---------|-------|"
+ echo "| Total | ${TOTAL:-0} |"
+ echo "| Pasadas | ${PASADAS:-0} |"
+ echo "| Fallidas | ${FALLIDAS:-0} |"
+ echo ""
+ echo "SDK usado en build: \`${{ needs.build.outputs.version-sdk }}\`"
+ } >> "$GITHUB_STEP_SUMMARY"
\ No newline at end of file
diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml
new file mode 100644
index 0000000..fc17534
--- /dev/null
+++ b/.github/workflows/Test.yml
@@ -0,0 +1,38 @@
+name: Test
+
+on:
+ push:
+ branches:
+ - main
+ - 'feature/**'
+ - 'fix/**'
+ pull_request:
+ branches:
+ - main
+
+env:
+ DOTNET_VERSION: '9.0.x'
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+
+jobs:
+ format:
+ name: Format check
+ runs-on: ubuntu-latest
+ outputs:
+ dotnet-version: ${{ steps.setup-dotnet.outputs.dotnet-version }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ id: setup-dotnet
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Restore dependencias
+ run: dotnet restore
+
+ - name: Verificar formato de código
+ run: dotnet format --verify-no-changes --verbosity diagnostic
\ No newline at end of file
diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml
index 2468a2c..ced22cb 100644
--- a/.github/workflows/matrix.yml
+++ b/.github/workflows/matrix.yml
@@ -43,15 +43,18 @@ jobs:
- name: Compilar
run: dotnet build --configuration Release --no-restore
-
+
- name: Ejecutar pruebas
- run: |
- dotnet test \
- --configuration Release \
- --no-restore \
- --collect:"XPlat Code Coverage" \
- --results-directory ./coverage \
- --logger "trx;LogFileName=test-results.trx"
+ run: dotnet test --configuration Release --no-restore --collect:"XPlat Code Coverage" --results-directory ./coverage --logger "trx;LogFileName=test-results.trx"
+
+ #- name: Ejecutar pruebas
+ # run: |
+ # dotnet test \
+ # --configuration Release \
+ # --no-restore \
+ # --collect:"XPlat Code Coverage" \
+ # --results-directory ./coverage \
+ # --logger "trx;LogFileName=test-results.trx"
- name: Subir resultado por combinación
uses: actions/upload-artifact@v4
diff --git a/src/FinancialUtils/Calculator.cs b/src/FinancialUtils/Calculator.cs
index 38323fe..c307709 100644
--- a/src/FinancialUtils/Calculator.cs
+++ b/src/FinancialUtils/Calculator.cs
@@ -12,6 +12,8 @@ public static class Calculator
public static int Add(int a, int b) => a + b;
+ public static float Add(float a, float b) => a + b;
+
///
/// Resta b de a.
///
@@ -29,7 +31,9 @@ public static class Calculator
public static decimal Divide(decimal a, decimal b)
{
if (b == 0)
+ {
throw new DivideByZeroException("El divisor no puede ser cero.");
+ }
return a / b;
}
@@ -46,13 +50,19 @@ public static decimal Divide(decimal a, decimal b)
public static decimal CompoundInterest(decimal principal, decimal rate, int periods)
{
if (principal < 0)
+ {
throw new ArgumentException("El capital no puede ser negativo.", nameof(principal));
+ }
if (rate < 0)
+ {
throw new ArgumentException("La tasa no puede ser negativa.", nameof(rate));
+ }
if (periods < 1)
+ {
throw new ArgumentException("Los periodos deben ser un entero positivo.", nameof(periods));
+ }
return principal * (decimal)Math.Pow((double)(1 + rate), periods);
}
@@ -69,16 +79,24 @@ public static decimal CompoundInterest(decimal principal, decimal rate, int peri
public static decimal LoanPayment(decimal principal, decimal annualRate, int months)
{
if (principal <= 0)
+ {
throw new ArgumentException("El préstamo debe ser mayor a cero.", nameof(principal));
+ }
if (annualRate < 0)
+ {
throw new ArgumentException("La tasa no puede ser negativa.", nameof(annualRate));
+ }
if (months < 1)
+ {
throw new ArgumentException("El plazo debe ser un entero positivo.", nameof(months));
+ }
if (annualRate == 0)
+ {
return Math.Round(principal / months, 2, MidpointRounding.AwayFromZero);
+ }
var monthlyRate = annualRate / 12;
var factor = (decimal)Math.Pow((double)(1 + monthlyRate), months);
@@ -103,7 +121,9 @@ public static decimal NetPresentValue(decimal discountRate, IEnumerable
?? throw new ArgumentNullException(nameof(cashFlows));
if (flows.Count == 0)
+ {
throw new ArgumentException("Se requiere al menos un flujo de caja.", nameof(cashFlows));
+ }
decimal npv = 0;
for (int t = 0; t < flows.Count; t++)
diff --git a/src/FinancialUtils/Formatter.cs b/src/FinancialUtils/Formatter.cs
index 25a942f..aa7e617 100644
--- a/src/FinancialUtils/Formatter.cs
+++ b/src/FinancialUtils/Formatter.cs
@@ -17,7 +17,9 @@ public static class Formatter
public static string FormatCurrency(decimal amount, string currencyCode = "USD", string cultureName = "es-MX")
{
if (string.IsNullOrWhiteSpace(currencyCode))
+ {
throw new ArgumentException("El código de moneda no puede estar vacío.", nameof(currencyCode));
+ }
var culture = new CultureInfo(cultureName);
var regionInfo = new RegionInfo(cultureName);
@@ -36,7 +38,9 @@ public static string FormatCurrency(decimal amount, string currencyCode = "USD",
public static string FormatPercentage(decimal value, int decimalPlaces = 2)
{
if (decimalPlaces < 0)
+ {
throw new ArgumentException("Los decimales no pueden ser negativos.", nameof(decimalPlaces));
+ }
return $"{(value * 100).ToString($"F{decimalPlaces}")}%";
}
@@ -50,7 +54,9 @@ public static string FormatPercentage(decimal value, int decimalPlaces = 2)
public static string FormatNumber(decimal value, int decimalPlaces = 0, string cultureName = "es-MX")
{
if (decimalPlaces < 0)
+ {
throw new ArgumentException("Los decimales no pueden ser negativos.", nameof(decimalPlaces));
+ }
var culture = new CultureInfo(cultureName);
return value.ToString($"N{decimalPlaces}", culture);
@@ -64,7 +70,9 @@ public static string FormatNumber(decimal value, int decimalPlaces = 0, string c
public static decimal TruncateDecimals(decimal value, int decimalPlaces)
{
if (decimalPlaces < 0)
+ {
throw new ArgumentException("Los decimales no pueden ser negativos.", nameof(decimalPlaces));
+ }
var factor = (decimal)Math.Pow(10, decimalPlaces);
return Math.Truncate(value * factor) / factor;
diff --git a/tests/FinancialUtils.Tests/CalculatorTests.cs b/tests/FinancialUtils.Tests/CalculatorTests.cs
index 6bab0b2..9d6ca78 100644
--- a/tests/FinancialUtils.Tests/CalculatorTests.cs
+++ b/tests/FinancialUtils.Tests/CalculatorTests.cs
@@ -26,6 +26,46 @@ public void Add_WithZero_ReturnsSameValue()
Assert.Equal(7m, Calculator.Add(0m, 7m));
}
+ // --- Add (int) ---
+
+ [Fact]
+ public void Add_Int_TwoPositives_ReturnsCorrectSum()
+ {
+ Assert.Equal(7, Calculator.Add(3, 4));
+ }
+
+ [Fact]
+ public void Add_Int_WithNegative_ReturnsCorrectSum()
+ {
+ Assert.Equal(1, Calculator.Add(-2, 3));
+ }
+
+ [Fact]
+ public void Add_Int_WithZero_ReturnsSameValue()
+ {
+ Assert.Equal(5, Calculator.Add(0, 5));
+ }
+
+ // --- Add (float) ---
+
+ [Fact]
+ public void Add_Float_TwoPositives_ReturnsCorrectSum()
+ {
+ Assert.Equal(5.5f, Calculator.Add(2.5f, 3.0f));
+ }
+
+ [Fact]
+ public void Add_Float_WithNegative_ReturnsCorrectSum()
+ {
+ Assert.Equal(1.5f, Calculator.Add(-1.0f, 2.5f));
+ }
+
+ [Fact]
+ public void Add_Float_WithZero_ReturnsSameValue()
+ {
+ Assert.Equal(4.2f, Calculator.Add(0f, 4.2f));
+ }
+
// --- Subtract ---
[Fact]
@@ -148,10 +188,10 @@ public void LoanPayment_InvalidMonths_ThrowsArgumentException(int months)
public void NetPresentValue_CalculatesCorrectly()
{
// Inversión inicial -1000, flujos futuros de 400 por 3 periodos a tasa 10%
- // NPV = -1000 + 400/1.1 + 400/1.21 + 400/1.331 ≈ -0.64
+ // NPV = -1000 + 400/1.1 + 400/1.21 + 400/1.331 ≈ -5.26
var cashFlows = new[] { -1000m, 400m, 400m, 400m };
var result = Calculator.NetPresentValue(0.10m, cashFlows);
- Assert.Equal(-0.64m, result);
+ Assert.Equal(-5.26m, result);
}
[Fact]
diff --git a/tests/FinancialUtils.Tests/GlobalUsings.cs b/tests/FinancialUtils.Tests/GlobalUsings.cs
index 8c927eb..c802f44 100644
--- a/tests/FinancialUtils.Tests/GlobalUsings.cs
+++ b/tests/FinancialUtils.Tests/GlobalUsings.cs
@@ -1 +1 @@
-global using Xunit;
\ No newline at end of file
+global using Xunit;