Array_map do PHP incluindo chaves

208

Existe uma maneira de fazer algo assim:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(array_map(function($a, $b) { return "$a loves $b"; }, 
         array_keys($test_array), 
         array_values($test_array)));

Mas, em vez de chamar array_keyse array_valuespassar diretamente a $test_arrayvariável?

A saída desejada é:

array(2) {
  [0]=>
  string(27) "first_key loves first_value"
  [1]=>
  string(29) "second_key loves second_value"
}
José Tomás Tocino
fonte
Veja também: stackoverflow.com/search?q=each_with_index de uma abordagem contrastante com este problema geral
dreftymac

Respostas:

206

Não com o array_map, pois ele não manipula chaves.

array_walk faz:

$test_array = array("first_key" => "first_value",
                    "second_key" => "second_value");
array_walk($test_array, function(&$a, $b) { $a = "$b loves $a"; });
var_dump($test_array);

// array(2) {
//   ["first_key"]=>
//   string(27) "first_key loves first_value"
//   ["second_key"]=>
//   string(29) "second_key loves second_value"
// }

No entanto, ele altera o array fornecido como parâmetro, portanto, não é exatamente uma programação funcional (como a pergunta foi marcada assim). Além disso, como apontado no comentário, isso alterará apenas os valores da matriz, para que as chaves não sejam as que você especificou na pergunta.

Você pode escrever uma função que corrige os pontos acima de si mesmo, se assim o desejar:

function mymapper($arrayparam, $valuecallback) {
  $resultarr = array();
  foreach ($arrayparam as $key => $value) {
    $resultarr[] = $valuecallback($key, $value);
  }
  return $resultarr;
}

$test_array = array("first_key" => "first_value",
                    "second_key" => "second_value");
$new_array = mymapper($test_array, function($a, $b) { return "$a loves $b"; });
var_dump($new_array);

// array(2) {
//   [0]=>
//   string(27) "first_key loves first_value"
//   [1]=>
//   string(29) "second_key loves second_value"
// }
eis
fonte
Exceto que, nesse caso, você deseja $a = "$b loves $a"corresponder à saída desejada do OP.
Cbuckley
1
correto, alterado :) é bom quão diferente eles fizeram array_map de array_walk.
eis
Bom obrigado. A fim de evitar mexer a matriz original, aqui está o que eu finalmente fez (olhar a minha resposta abaixo)
José Tomás Tocino
3
Porém, array_walk()isso não é "programação funcional", já que não retorna a matriz resultante, mas um bool.
mae
@mae sim, como eu escrevi na resposta, bem como - em vez de devolver o valor muda o parâmetro
eis
145

Este é provavelmente o mais curto e fácil de raciocinar sobre:

$states = array('az' => 'Arizona', 'al' => 'Alabama');

array_map(function ($short, $long) {
    return array(
        'short' => $short,
        'long'  => $long
    );
}, array_keys($states), $states);

// produces:
array(
     array('short' => 'az', 'long' => 'Arizona'), 
     array('short' => 'al', 'long' => 'Alabama')
)
Kevin Beal
fonte
15
Acabei de perceber que a pergunta dizia especificamente para não usar array_keys(). Isso parece um requisito bobo, no entanto.
Kevin Beal
3
A pergunta forneceu uma solução usando array_keys (); seria tolo fornecer uma resposta que não tem vantagem (por exemplo, chamar menos funções) sobre a solução atual.
Chinoto Vokro 29/03/16
A resposta para a pergunta original é NÃO, e esta é a solução mais apropriada.
usoban
65

Aqui está minha solução muito simples e compatível com PHP 5.5:

function array_map_assoc(callable $f, array $a) {
    return array_column(array_map($f, array_keys($a), $a), 1, 0);
}

A chamada que você fornece deve retornar uma matriz com dois valores, ou seja return [key, value]. A chamada interna para, array_mapportanto, produz uma matriz de matrizes. Isso é convertido novamente em uma matriz de dimensão única por array_column.

Uso

$ordinals = [
    'first' => '1st',
    'second' => '2nd',
    'third' => '3rd',
];

$func = function ($k, $v) {
    return ['new ' . $k, 'new ' . $v];
};

var_dump(array_map_assoc($func, $ordinals));

Resultado

array(3) {
  ["new first"]=>
  string(7) "new 1st"
  ["new second"]=>
  string(7) "new 2nd"
  ["new third"]=>
  string(7) "new 3rd"
}

Aplicação parcial

Caso você precise usar a função várias vezes com matrizes diferentes, mas com a mesma função de mapeamento, é possível fazer algo chamado aplicativo de função parcial (relacionado a ' currying '), que permite transmitir apenas o array de dados após a chamada:

function array_map_assoc_partial(callable $f) {
    return function (array $a) use ($f) {
        return array_column(array_map($f, array_keys($a), $a), 1, 0);
    };
}

...
$my_mapping = array_map_assoc_partial($func);
var_dump($my_mapping($ordinals));

Que produz a mesma saída, dada $funce $ordinalsé a anterior.

NOTA: se a sua função mapeada retornar a mesma tecla para duas entradas diferentes, o valor associado à tecla posterior ganhará. Inverta a matriz de entrada e o resultado da saída array_map_assocpara permitir que as chaves anteriores sejam vencidas. (As chaves retornadas no meu exemplo não podem colidir porque incorporam a chave da matriz de origem, que por sua vez deve ser única.)


Alternativo

A seguir está uma variante do acima, que pode ser mais lógico para alguns, mas requer o PHP 5.6:

function array_map_assoc(callable $f, array $a) {
    return array_merge(...array_map($f, array_keys($a), $a));
}

Nesta variante, sua função fornecida (sobre a qual a matriz de dados é mapeada) deve retornar uma matriz associativa com uma linha, ou seja return [key => value]. O resultado do mapeamento da chamada é simplesmente descompactado e passado para array_merge. Como antes, o retorno de uma chave duplicada resultará na conquista de valores posteriores.

Nota: Alex83690 observou em um comentário que o uso array_replaceaqui no lugar de array_mergepreservaria chaves inteiras. array_replacenão modifica a matriz de entrada, portanto, é seguro para código funcional.

Se você estiver no PHP 5.3 a 5.5, o seguinte é equivalente. Ele usa array_reducee o +operador de matriz binária para converter a matriz bidimensional resultante em uma matriz unidimensional, preservando as chaves:

function array_map_assoc(callable $f, array $a) {
    return array_reduce(array_map($f, array_keys($a), $a), function (array $acc, array $a) {
        return $acc + $a;
    }, []);
}

Uso

Ambas as variantes seriam usadas assim:

$ordinals = [
    'first' => '1st',
    'second' => '2nd',
    'third' => '3rd',
];

$func = function ($k, $v) {
    return ['new ' . $k => 'new ' . $v];
};

var_dump(array_map_assoc($func, $ordinals));

Observe o em =>vez de ,em $func.

A saída é a mesma de antes e cada uma pode ser parcialmente aplicada da mesma maneira que antes.


 Resumo

O objetivo da pergunta original é tornar a chamada a mais simples possível, às custas de ter uma função mais complicada que é chamada; especialmente, ter a capacidade de transmitir a matriz de dados como um único argumento, sem dividir as chaves e os valores. Usando a função fornecida no início desta resposta:

$test_array = ["first_key" => "first_value",
               "second_key" => "second_value"];

$array_map_assoc = function (callable $f, array $a) {
    return array_column(array_map($f, array_keys($a), $a), 1, 0);
};

$f = function ($key, $value) {
    return [$key, $key . ' loves ' . $value];
};

var_dump(array_values($array_map_assoc($f, $test_array)));

Ou, apenas para esta pergunta, podemos simplificar a array_map_assoc()função que descarta as teclas de saída, pois a pergunta não as pede:

$test_array = ["first_key" => "first_value",
               "second_key" => "second_value"];

$array_map_assoc = function (callable $f, array $a) {
    return array_map($f, array_keys($a), $a);
};

$f = function ($key, $value) {
    return $key . ' loves ' . $value;
};

var_dump($array_map_assoc($f, $test_array));

Portanto, a resposta é NÃO , você não pode evitar ligar array_keys, mas pode abstrair o local em que array_keysé chamado para uma função de ordem superior, o que pode ser bom o suficiente.

Nicholas Shanks
fonte
7
Parece que isso deve ser marcado como a resposta correta.
precisa saber é o seguinte
6
Obrigado @eddiewould, mas estou atrasado cerca de 4 anos e meio :) Vim para cá procurando uma solução, não encontrei nenhuma que gostei, então criei a minha.
Nicholas Shanks
1
Eu serei esse cara. O PHP 5.3 não deve mais ser um requisito para a data desta resposta. NA MINHA HUMILDE OPINIÃO.
Erutan409
1
Sua primeira solução alternativa é inválida. você deve substituir array_mergepor array_replacepara preservar as chaves que seriam números inteiros.
precisa saber é o seguinte
1
@ Alex83690 Obrigado! Embora eu diria que "inválido" é um pouco enganador - tudo bem se você não tiver nenhuma chave inteira (como aconteceu no meu caso).
Nicholas Shanks
20

Com o PHP5.3 ou posterior:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(
    array_map(
        function($key) use ($test_array) { return "$key loves ${test_array[$key]}"; },
        array_keys($test_array)
    )
);
Tadas Sasnauskas
fonte
1
Eu acho que o requisito era "em vez de chamar array_keys e array_values, passando diretamente a variável $ test_array", isso pode ser usado sem array_keys?
eis
4

É assim que eu implementei isso no meu projeto.

function array_map_associative(callable $callback, $array) {
    /* map original array keys, and call $callable with $key and value of $key from original array. */
    return array_map(function($key) use ($callback, $array){
        return $callback($key, $array[$key]);
    }, array_keys($array));
}
Jijo
fonte
Muito limpo e não altera a matriz original!
Raffaele Candeliere
4

Olhe aqui! Existe uma solução trivial!

function array_map2(callable $f, array $a)
{
    return array_map($f, array_keys($a), $a);
}

Como afirmado na pergunta, array_map já possui exatamente a funcionalidade necessária . As outras respostas aqui complicam seriamente demais as coisas: array_walknão é funcional.

Uso

Exatamente como você esperaria do seu exemplo:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(array_map2(function($a, $b) { return "$a loves $b"; }, $test_array));
IanS
fonte
as outras respostas complicam demais as coisas porque a pergunta especificada qrrqy_keys()não deve ser usada para # razões
Brad Kent
2

Por "loop manual", quis dizer escrever uma função personalizada que usa foreach. Isso retorna uma nova matriz, como array_mapacontece porque o escopo da função faz $arraycom que seja uma cópia - não uma referência:

function map($array, callable $fn) {
  foreach ($array as $k => &$v) $v = call_user_func($fn, $k, $v);
  return $array;
}

Sua técnica usando array_mapwith, array_keysna verdade, parece mais simples e mais poderosa, porque você pode usar nullcomo retorno de chamada para retornar os pares de valores-chave:

function map($array, callable $fn = null) {
  return array_map($fn, array_keys($array), $array);
}
ryanve
fonte
looping array com referência, pode causar coisas assustadores acontecer
janenz00
Não é assustador, apenas significa que você esqueceu, unset( $value )porque ele ainda existe no escopo definido.
Aziz punjani
@azis, estava brincando sobre o fantasma, referindo-se ao artigo. Isso criará efeitos inesperados se você esquecer de cancelar a configuração.
janenz00
1
Obrigado pela resposta, mas achei bem claro que não queria usar um loop tradicional.
José Tomás Tocino 23/10/12
@ janenz00 Veja a resposta editada para esclarecimentos. Eu quis dizer looping em um escopo de variável limpa.
Ryanve
1

Com base na resposta da eis , aqui está o que eu fiz para evitar mexer na matriz original:

$test_array = array("first_key" => "first_value",
                    "second_key" => "second_value");

$result_array = array();
array_walk($test_array, 
           function($a, $b) use (&$result_array) 
           { $result_array[] = "$b loves $a"; }, 
           $result_array);
var_dump($result_array);
José Tomás Tocino
fonte
2
Por que isso é mais fácil do que apenas passar os valores e as chaves da matriz diretamente para o array_map? É mais lento e mais complicado, não estou vendo a vantagem.
Ariel
1
@Ariel, você pode apoiar a afirmação de que seria mais lento, mesmo com grandes números? Ele precisa iterar o array apenas uma vez, então acho que as magnitudes devem ser mais rápidas na grande notação O. Mas eu concordo com a complexidade.
eis
@eis É mais lento porque está criando a matriz de resultados, uma de cada vez, em PHP, em vez de em massa em C. Evita a chamada de array_keys (embora seja rápido, pois está em C). Faça um benchmark - veja o que é mais rápido, não tenho muita certeza, mas geralmente mais código = código mais lento. Na complexidade, é definitivamente pior, e isso é mais importante do que acelerar a maior parte do tempo.
Ariel
1
Você não precisa enviar o terceiro argumento para, array_walkpois não o está referenciando no encerramento.
Steven Lu
1

Eu fiz essa função, com base na resposta de eis :

function array_map_($callback, $arr) {
    if (!is_callable($callback))
        return $arr;

    $result = array_walk($arr, function(&$value, $key) use ($callback) {
        $value = call_user_func($callback, $key, $value);
    });

    if (!$result)
        return false;

    return $arr;
}

Exemplo:

$test_array = array("first_key" => "first_value", 
                "second_key" => "second_value");

var_dump(array_map_(function($key, $value){
    return $key . " loves " . $value;
}, $arr));

Resultado:

array (
  'first_key' => 'first_key loves first_value,
  'second_key' => 'second_key loves second_value',
)

Obviamente, você pode usar array_valuespara retornar exatamente o que o OP deseja.

array_values(array_map_(function($key, $value){
    return $key . " loves " . $value;
}, $test_array))
Julio Vedovatto
fonte
@ KevinBeal Eu uso muito essa função no meu trabalho. Você poderia apontar onde estão os erros?
Julio Vedovatto
2
Primeiramente, o código como está está faltando uma verificação $arrdo tipo array, no entanto, se você digitar seus argumentos como callablee arraypoderá, em vez disso, soltar a verificação em is_callable. Em seguida, você faz uma atribuição ao valor $ que não é usada. Você deve apenas ignorar o valor de retorno. Em terceiro lugar, seria melhor lançar uma exceção no retorno de chamada do que retornar false. Você sempre retornaria um valor válido ou sempre lançaria.
Nicholas Shanks
1

A biblioteca do YaLinqo * é adequada para esse tipo de tarefa. É uma porta do LINQ from .NET que suporta totalmente valores e chaves em todos os retornos de chamada e se assemelha a SQL. Por exemplo:

$mapped_array = from($test_array)
    ->select(function ($v, $k) { return "$k loves $v"; })
    ->toArray();

ou apenas:

$mapped_iterator = from($test_array)->select('"$k loves $v"');

Aqui '"$k loves $v"'está um atalho para a sintaxe de fechamento completo, suportada por esta biblioteca. toArray()no final é opcional. A cadeia de métodos retorna um iterador. Portanto, se o resultado precisar ser iterado usando foreach,toArray chamada poderá ser removida.

* desenvolvido por mim

Athari
fonte
1

Eu faria algo assim:

<?php

/**
 * array_map_kv()
 *   An array mapping function to map with both keys and values.
 *
 * @param $callback callable
 *   A callback function($key, $value) for mapping values.
 * @param $array array
 *   An array for mapping.
 */
function array_map_kv(callable $callback, array $array) {
  return array_map(
    function ($key) use ($callback, $array) {
      return $callback($key, $array[$key]); // $callback($key, $value)
    },
    array_keys($array)
  );
}

// use it
var_dump(array_map_kv(function ($key, $value) {
  return "{$key} loves {$value}";
}, array(
  "first_key" => "first_value",
  "second_key" => "second_value",
)));

?>

Resultados:

array(2) {
  [0]=>
  string(27) "first_key loves first_value"
  [1]=>
  string(29) "second_key loves second_value"
}
Koala Yeung
fonte
1

Adicionarei outra solução ao problema usando a versão 5.6 ou posterior. Não sei se é mais eficiente do que as já excelentes soluções (provavelmente não), mas para mim é mais simples de ler:

$myArray = [
    "key0" => 0,
    "key1" => 1,
    "key2" => 2
];

array_combine(
    array_keys($myArray),
    array_map(
        function ($intVal) {
            return strval($intVal);
        },
        $myArray
    )
);

Usando strval()como uma função de exemplo no array_map, isso irá gerar:

array(3) {
  ["key0"]=>
  string(1) "0"
  ["key1"]=>
  string(1) "1"
  ["key2"]=>
  string(1) "2"
}

Espero que eu não seja o único que ache isso muito simples de entender. array_combinecria uma key => valuematriz a partir de uma matriz de chaves e de uma matriz de valores, o resto é bastante autoexplicativo.

Francesco DM
fonte
1

Você pode usar o método map nesta biblioteca de matrizes para obter exatamente o que deseja tão facilmente quanto:

Arr::map($test_array, function($a, $b) { return "$a loves $b"; });

também preserva chaves e retorna nova matriz, para não mencionar alguns modos diferentes para atender às suas necessidades.

Minwork
fonte
1
$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

$new = array_map(function($key, $value) {
  return "{$key} => {$value}";
}, array_keys($array), $array);

Fonte

Ostap B.
fonte
0

Eu sempre gosto da variante javascript do mapa de matriz. A versão mais simples seria:

/**
 * @param  array    $array
 * @param  callable $callback
 * @return array
 */
function arrayMap(array $array, callable $callback)
{
    $newArray = [];

    foreach( $array as $key => $value )
    {
        $newArray[] = call_user_func($callback, $value, $key, $array);
    }

    return $newArray;
}

Portanto, agora você pode passar uma função de retorno de chamada como construir os valores.

$testArray = [
    "first_key" => "first_value", 
    "second_key" => "second_value"
];

var_dump(
    arrayMap($testArray, function($value, $key) {
        return $key . ' loves ' . $value;
    });
);
blablabla
fonte
É mais útil ter os dados como o último argumento para qualquer função que você escreve, pois é possível criar uma nova função que incorpora algum retorno de chamada específico (comportamento) - ou seja, você obtém a composição da função: h(g(f($data)))aplica - se f, então g, hao seu dados. Geralmente, é considerado mais versátil na programação funcional ter uma função que execute a mesma operação em diversos dados do que ter uma função que aplique diversas funções a um conjunto de dados fixo.
Nicholas Shanks
No seu exemplo, você tem apenas 1 argumento para a função. Acho mais fácil colocar os dados como primeiro argumento, como array_filter, array_reduce e as funções de matriz em javascript.
22818
Esse é meu argumento! Ao passar os dados passado, ele permite que você caril a função (criar uma nova função que combina o looping com a operação específica) e aplicá que os dados chamando a nova função com um único parâmetro. Este princípio é explicado melhor do que eu aqui, nesta resposta: stackoverflow.com/a/5863222
Nicholas Shanks
O uso de uma função de composição em uma linguagem como PHP não é uma solução melhor para esse problema?
22818
1
É uma alternativa, mas requer substancialmente mais investimento em FP, por exemplo: github.com/nickshanks/fp-php-talk/blob/master/lib.php#L24 ou: github.com/nickshanks/php-fp/blob /master/src/fp.php#L62
Nicholas Shanks
0

Outra maneira de fazer isso com a preservação de chaves (fora):

$test_array = [
    "first_key"     => "first_value",
    "second_key"    => "second_value"
];

$f = function($ar) {
    return array_map(
        function($key, $val) {
            return "{$key} - {$val}";
        },
        array_keys($ar),
        $ar
    );
};

#-- WITHOUT preserving keys
$res = $f($test_array);

#-- WITH preserving keys
$res = array_combine(
    array_keys($test_array),
    $f($test_array)
);
Blablaenzo
fonte
-2

Vejo que falta a resposta óbvia:

function array_map_assoc(){
    if(func_num_args() < 2) throw new \BadFuncionCallException('Missing parameters');

    $args = func_get_args();
    $callback = $args[0];

    if(!is_callable($callback)) throw new \InvalidArgumentException('First parameter musst be callable');

    $arrays = array_slice($args, 1);

    array_walk($arrays, function(&$a){
        $a = (array)$a;
        reset($a);
    });

    $results = array();
    $max_length = max(array_map('count', $arrays));

    $arrays = array_map(function($pole) use ($max_length){
        return array_pad($pole, $max_length, null);
    }, $arrays);

    for($i=0; $i < $max_length; $i++){
        $elements = array();
        foreach($arrays as &$v){
            $elements[] = each($v);
        }
        unset($v);

        $out = call_user_func_array($callback, $elements);

        if($out === null) continue;

        $val = isset($out[1]) ? $out[1] : null;

        if(isset($out[0])){
            $results[$out[0]] = $val;
        }else{
            $results[] = $val;
        }
    }

    return $results;
}

Funciona exatamente como array_map. Quase.

Na verdade, não é puro mapcomo você o conhece em outros idiomas. Php é muito estranho, portanto, requer algumas funções de usuário muito estranhas, pois não queremos desvendar nossa worse is betterabordagem precisamente quebrada .

Realmente, não é realmente map. No entanto, ainda é muito útil.

  • A primeira diferença óbvia do array_map é que o retorno de chamada obtém saídas de each()cada array de entrada, e não apenas do valor. Você ainda pode percorrer mais matrizes de uma só vez.

  • A segunda diferença é a maneira como a chave é manipulada após retornar do retorno de chamada; o valor de retorno da função de retorno de chamada deve ser array('new_key', 'new_value'). As chaves podem e serão alteradas, as mesmas chaves podem até substituir o valor anterior, se a mesma chave for retornada. Esse não é um mapcomportamento comum , mas permite reescrever as chaves.

  • A terceira coisa estranha é que, se você omitir o keyvalor de retorno (por array(1 => 'value')ou array(null, 'value')), a nova chave será atribuída, como se $array[] = $valuefosse usada. Esse também não mapé um comportamento comum, mas às vezes é útil, eu acho.

  • A quarta coisa estranha é que, se a função de retorno de chamada não retornar um valor, ou retornar null, todo o conjunto de chaves e valores atuais é omitido na saída, é simplesmente ignorado. Esse recurso é totalmente un mappy, mas tornaria essa função um excelente golpe para duplicar array_filter_assoc, se houvesse essa função.

  • Se você omitir o segundo elemento ( 1 => ...) (a parte do valor ) no retorno do retorno de chamada, nullserá usado em vez do valor real.

  • Quaisquer outros elementos, exceto aqueles com chaves 0e 1no retorno de retorno de chamada, são ignorados.

  • E, finalmente, se lambda retornar qualquer valor, exceto de nullou matriz, será tratado como se a chave e o valor fossem omitidos, portanto:

    1. nova chave para o elemento é atribuída
    2. null é usado como seu valor
AVISO:
Lembre-se de que esse último recurso é apenas um resíduo dos recursos anteriores e provavelmente é completamente inútil. Confiar nesse recurso é altamente desencorajado, pois esse recurso será descontinuado aleatoriamente e alterado inesperadamente em versões futuras.

NOTA:
Ao contrário de array_map, todos os parâmetros que não são de matriz passados ​​para array_map_assoc, com exceção do primeiro parâmetro de retorno de chamada, são convertidos silenciosamente em matrizes.

EXEMPLOS:
// TODO: examples, anyone?

enrey
fonte