Douglas Moura

Douglas Moura

Engenheiro de Software

Douglas Moura

Douglas Moura

Eu escrevo sobre TypeScript, React e Node.js.

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

Publicado em:Publicado em:Atualizado em:

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.

Deixe um comentário

Carregando comentários...