Douglas Moura

Douglas Moura

Engenheiro de Software

Douglas Moura

Douglas Moura

Engenheiro de software, músico and jiujiteiro

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...