Pular para o conteúdo

Múltiplas formas de somar os valores de um array de objetos, em TypeScript

· 2 min. de leitura

Múltiplas formas de somar os valores de um array de objetos, em TypeScript

Há algum tempo, o Zan Franceschi postou o seguinte desafio:

Neste artigo, vou resolvê-lo de duas formas: uma com laços e outra sem laços, com TypeScript.

Analisando os dados

Vou transcrever o JSON do desafio para facilitar a nossa análise:

{
  "compras": [
    {
      "data": "2022-01-01",
      "produtos": [
        {
          "cod": "a",
          "qtd": 2,
          "valor_unitario": 12.24
        },
        {
          "cod": "b",
          "qtd": 1,
          "valor_unitario": 3.99
        },
        {
          "cod": "c",
          "qtd": 3,
          "valor_unitario": 98.14
        }
      ]
    },
    {
      "data": "2022-01-02",
      "produtos": [
        {
          "cod": "a",
          "qtd": 6,
          "valor_unitario": 12.34
        },
        {
          "cod": "b",
          "qtd": 1,
          "valor_unitario": 3.99
        },
        {
          "cod": "c",
          "qtd": 1,
          "valor_unitario": 34.02
        }
      ]
    }
  ]
}

Podemos definir o JSON acima em dois tipos distintos: Produto e Compra. Seus nomes são autoexplicativos, mas vou defini-los aqui para facilitar a leitura do código:

type Produto = {
  cod: string;
  qtd: number;
  valor_unitario: number;
};

type Compra = {
  data: string;
  produtos: Produto[];
};

Escrevendo testes

Antes de começar a escrever o código, vamos escrever alguns testes para garantir que o código está funcionando corretamente. Para isso, vamos usar o Vitest.

import { describe, expect, test } from "vitest";
import {
  somaComFor,
  somaComForIn,
  somaComForOf,
  somaComWhile,
  somaComForEach,
  somaComReduce,
  somaComFlatMapEReduce,
  somaComSum,
  somaComSumEFlatMap,
} from "./desafio-da-soma";

const { compras } = await import("./data.json", { with: { type: "json" } });

const result = 434.93999999999994;

describe("Desafio da soma", () => {
  test("somaComFor", () => {
    expect(somaComFor(compras)).toBe(result);
  });

  test("somaComForOf", () => {
    expect(somaComForOf(compras)).toBe(result);
  });

  test("somaComForIn", () => {
    expect(somaComForIn(compras)).toBe(result);
  });

  test("somaComWhile", () => {
    expect(somaComWhile(compras)).toBe(result);
  });

  test("somaComForEach", () => {
    expect(somaComForEach(compras)).toBe(result);
  });

  test("somaComReduce", () => {
    expect(somaComReduce(compras)).toBe(result);
  });

  test("somaComFlatMapEReduce", () => {
    expect(somaComFlatMapEReduce(compras)).toBe(result);
  });

  test("somaComSum", () => {
    expect(somaComSum(compras)).toBe(result);
  });

  test("somaComSumEFlatMap", () => {
    expect(somaComSumEFlatMap(compras)).toBe(result);
  });
});

Somando com laços

Agora que já temos os tipos definidos, podemos começar a resolver o desafio. Podemos fazer isso de quatro formas:

1. Usando for

O for é um laço de repetição que executa um bloco de código até que uma condição seja satisfeita.

function somaComFor(compras: Compra[]): number {
  let soma = 0;

  for (let i = 0; i < compras.length; i++) {
    for (let j = 0; j < compras[i].produtos.length; j++) {
      soma +=
        compras[i].produtos[j].qtd * compras[i].produtos[j].valor_unitario;
    }
  }

  return soma;
}

2. Usando for...of

O for...of é um laço de iteração que itera sobre uma sequência de valores vindos de um objeto iterável (que, no nosso caso, é um Array).

function somaComForOf(compras: Compra[]): number {
  let soma = 0;

  for (const compra of compras) {
    for (const produto of compra.produtos) {
      soma += produto.qtd * produto.valor_unitario;
    }
  }

  return soma;
}

3. Usando for...in

O for...in é um laço de iteração que itera sobre as propriedades enumeráveis do tipo string de um objeto, inclusive as herdadas. Estou colocando esta solução aqui somente para mostrar que ela é possível, mas deve ser preterida em relação às outras soluções apresentadas neste artigo.

function somaComForIn(compras: Compra[]): number {
  let soma = 0;

  for (const i in compras) {
    for (const j in compras[i].produtos) {
      soma +=
        compras[i].produtos[j].qtd * compras[i].produtos[j].valor_unitario;
    }
  }

  return soma;
}

4. Usando while

O while é um laço de repetição que executa um bloco de código até que uma condição seja satisfeita.

function somaComWhile(compras: Compra[]): number {
  let soma = 0;

  let i = 0;
  while (i < compras.length) {
    let j = 0;
    while (j < compras[i].produtos.length) {
      soma +=
        compras[i].produtos[j].qtd * compras[i].produtos[j].valor_unitario;
      j++;
    }
    i++;
  }

  return soma;
}

Somando com métodos de Array

1. Usando Array.prototype.forEach

O Array.prototype.forEach é um método que executa uma função para cada elemento do array.

function somaComForEach(compras: Compra[]): number {
  let soma = 0;

  compras.forEach((compra) => {
    compra.produtos.forEach((produto) => {
      soma += produto.qtd * produto.valor_unitario;
    });
  });

  return soma;
}

2. Usando Array.prototype.reduce

O Array.prototype.reduce é um método que executa uma função para cada elemento do array, retornando um único valor.

function somaComReduce(compras: Compra[]): number {
  return compras.reduce((soma, compra) => {
    return (
      soma +
      compra.produtos.reduce((soma, produto) => {
        return soma + produto.qtd * produto.valor_unitario;
      }, 0)
    );
  }, 0);
}

Mas, podemos torná-la ainda melhor usando o Array.prototype.flatMap:

function somaComFlatMapEReduce(compras: Compra[]): number {
  return compras
    .flatMap((compra) => compra.produtos)
    .reduce((soma, produto) => soma + produto.qtd * produto.valor_unitario, 0);
}

Com uma função de ajuda

Podemos criar uma função genérica que faz a soma de qualquer array da seguinte forma:

/**
 * Soma todos os valores de um array. Se o array for de objetos, é possível
 * passar uma função para extrair os números que serão somados.
 *
 * @example soma([1, 2, 3]) // 6
 * @example soma([{ valor: 1 }, { valor: 2 }, { valor: 3 }], item => item.valor) // 6
 */
function sum<T extends number | object>(
  array: T[],
  fn?: (item: T) => number
): number {
  return array.reduce(
    (acumulador, item) => acumulador + (fn ? fn(item) : (item as number)),
    0
  );
}

E usá-la em conjunto com as soluções já apresentadas:

function somaComSumEFlatMap(compras: Compra[]): number {
  return sum(
    compras.flatMap((compra) => compra.produtos),
    (produto) => produto.qtd * produto.valor_unitario
  );
}

Se tiver alguma sugestão ou alguma outra ideia de como resolver este desafio, conta para mim nos comentários.