javascript - Ramda:折叠一个对象
问题描述
我正在构建一个 PWA 并使用 Ramda 进行逻辑构建。我正在尝试构建一个给定Google Places Detail 响应返回自定义地址对象的函数。
让我通过向您展示我的测试来用代码描述它:
assert({
given: 'a google places api response from Google Places',
should: 'extract the address',
actual: getAddressValues({
address_components: [
{
long_name: '5',
short_name: '5',
types: ['floor'],
},
{
long_name: '48',
short_name: '48',
types: ['street_number'],
},
{
long_name: 'Pirrama Road',
short_name: 'Pirrama Rd',
types: ['route'],
},
{
long_name: 'Pyrmont',
short_name: 'Pyrmont',
types: ['locality', 'political'],
},
{
long_name: 'Council of the City of Sydney',
short_name: 'Sydney',
types: ['administrative_area_level_2', 'political'],
},
{
long_name: 'New South Wales',
short_name: 'NSW',
types: ['administrative_area_level_1', 'political'],
},
{
long_name: 'Australia',
short_name: 'AU',
types: ['country', 'political'],
},
{
long_name: '2009',
short_name: '2009',
types: ['postal_code'],
},
],
geometry: {
location: {
lat: -33.866651,
lng: 151.195827,
},
viewport: {
northeast: {
lat: -33.8653881697085,
lng: 151.1969739802915,
},
southwest: {
lat: -33.86808613029149,
lng: 151.1942760197085,
},
},
},
}),
expected: {
latitude: -33.866651,
longitude: 151.195827,
city: 'Pyrmont',
zipCode: '2009',
streetName: 'Pirrama Road',
streetNumber: '48',
},
});
如您所见,我想要的地址对象更“扁平”(因为缺少更好的术语)。我正在努力编写这个转换函数。我尝试使用 Ramda's 进行操作evolve
,但保留了密钥。我需要使用进化来转换对象,然后reduce
对象传播键。
// Pseudo
({ address_components }) => ({ ...address_components })
我使用 Ramda adjunct 成功提取了相关信息并重evolve
命名了键renameKeys
,但我不知道之后如何展平该对象。你是怎样做的?或者是否有更简单的方法来实现所需的转换?
编辑:
我找到了一种方法来实现我的转变,但它非常冗长。我觉得有一种更简单的方法来提取地址数据。无论如何,这是我目前的解决方案:
export const getAddressValues = pipe(
evolve({
address_components: pipe(
reduce(
(acc, val) => ({
...acc,
...{
[head(prop('types', val))]: prop('long_name', val),
},
}),
{}
),
pipe(
pickAll([
'route',
'locality',
'street_number',
'country',
'postal_code',
]),
renameKeys({
route: 'streetName',
locality: 'city',
street_number: 'streetNumber',
postal_code: 'zipCode',
}),
map(ifElse(isNil, always(null), identity))
)
),
geometry: ({ location: { lat, lon } }) => ({
latitude: lat,
longitude: lon,
}),
}),
({ address_components, geometry }) => ({ ...address_components, ...geometry })
);
编辑:基于@codeepic 的回答,这是我最终使用的纯 JavaScript 解决方案(尽管@user3297291 很优雅,我喜欢它):
const getLongNameByType = (arr, type) =>
arr.find(o => o.types.includes(type)).long_name;
const getAddressValues = ({ address_components: comp, geometry: { location: { lat, lng } } }) => ({
latitude: lat,
longitude: lng,
city: getLongNameByType(comp, 'locality'),
zipCode: getLongNameByType(comp, 'postal_code'),
streetName: getLongNameByType(comp, 'route'),
streetNumber: getLongNameByType(comp, 'street_number'),
country: getLongNameByType(comp, 'country'),
});
解决方案
镜头可能是你最好的选择。Ramda 有一个通用lens
函数,以及针对对象属性 ( lensProp
)、数组索引 ( lensIndex
) 和更深路径 ( lensPath
) 的特定函数,但它不包括通过 id 在数组中查找匹配值的函数。不过,自己制作并不难。
镜头是通过将两个函数传递给lens
:一个获取对象并返回相应值的获取器,以及一个获取新值和对象并返回对象的更新版本的设置器。
在这里,我们编写lensMatch
which 在给定属性名称与提供的值匹配的数组中查找或设置值。并lensType
简单地传递'type'
给lensMatch
返回一个函数,该函数将采用一组类型并返回一个镜头。
使用任何镜头,我们都有view
、set
和over
函数,它们分别获取、设置和更新值。
const lensMatch = (propName) => (key) => lens (
find ( propEq (propName, key) ),
(val, arr, idx = findIndex (propEq (propName, key), arr)) =>
update(idx > -1 ? idx : length(arr), val, arr)
)
const lensTypes = lensMatch ('types')
const longName = (types) =>
compose (lensProp ('address_components'), lensTypes (types), lensProp ('long_name'))
// can define `shortName` similarly if needed
const getAddressValues = applySpec ( {
latitude: view (lensPath (['geometry', 'location', 'lat']) ),
longitude: view (lensPath (['geometry', 'location', 'lng']) ),
city: view (longName (['locality', 'political']) ),
zipCode: view (longName (['postal_code']) ),
streetName: view (longName (['route']) ),
streetNumber: view (longName (['street_number']) ),
})
const response = {"address_components": [{"long_name": "5", "short_name": "5", "types": ["floor"]}, {"long_name": "48", "short_name": "48", "types": ["street_number"]}, {"long_name": "Pirrama Road", "short_name": "Pirrama Rd", "types": ["route"]}, {"long_name": "Pyrmont", "short_name": "Pyrmont", "types": ["locality", "political"]}, {"long_name": "Council of the City of Sydney", "short_name": "Sydney", "types": ["administrative_area_level_2", "political"]}, {"long_name": "New South Wales", "short_name": "NSW", "types": ["administrative_area_level_1", "political"]}, {"long_name": "Australia", "short_name": "AU", "types": ["country", "political"]}, {"long_name": "2009", "short_name": "2009", "types": ["postal_code"]}], "geometry": {"location": {"lat": -33.866651, "lng": 151.195827}, "viewport": {"northeast": {"lat": -33.8653881697085, "lng": 151.1969739802915}, "southwest": {"lat": -33.86808613029149, "lng": 151.1942760197085}}}}
console .log (
getAddressValues (response)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script><script>
const {applySpec, compose, find, findIndex, lens, lensProp, lensPath, propEq, update, view} = R </script>
我们可以用一个更简单的版本来lensMatch
解决这个问题,因为我们没有使用 setter:
const lensMatch = (propName) => (key) =>
lens (find (propEq (propName, key) ), () => {} )
但我不会推荐它。fulllensMatch
是一个有用的效用函数。
有几种方法我们可能想要改变这个解决方案。我们可以移动view
内部longName
并编写另一个小助手来包装 in 的结果,lensPath
以view
简化调用看起来更像这样。
longitude: viewPath (['geometry', 'location', 'lng']),
city: longName (['locality', 'political']),
或者我们可以为 编写一个包装器applySpec
,也许viewSpec
它只是将所有属性函数包装在view
. 这些留给读者作为练习。
(对此的介绍几乎没有从我早期的答案中修改过。)
更新
我还尝试了一种完全独立的方法。我认为它的可读性较差,但它可能性能更高。比较选项很有趣。
const makeKey = JSON.stringify
const matchType = (name) => (
spec,
desc = spec.reduce( (a, [t, n]) => ({...a, [makeKey (t)]: n}), {})
) => (xs) => xs.reduce(
(a, { [name]: fld, types }, _, __, k = makeKey(types)) => ({
...a,
...(k in desc ? {[desc[k]]: fld} : {})
}),
{}
)
const matchLongNames = matchType('long_name')
const getAddressValues2 = lift (merge) (
pipe (
prop ('address_components'),
matchLongNames ([
[['locality', 'political'], 'city'],
[['postal_code'], 'zipCode'],
[['route'], 'streetName'],
[['street_number'], 'streetNumber'],
])
),
applySpec ({
latitude: path(['geometry', 'location', 'lat']),
longitude: path(['geometry', 'location', 'lng']),
})
)
const response = {"address_components": [{"long_name": "5", "short_name": "5", "types": ["floor"]}, {"long_name": "48", "short_name": "48", "types": ["street_number"]}, {"long_name": "Pirrama Road", "short_name": "Pirrama Rd", "types": ["route"]}, {"long_name": "Pyrmont", "short_name": "Pyrmont", "types": ["locality", "political"]}, {"long_name": "Council of the City of Sydney", "short_name": "Sydney", "types": ["administrative_area_level_2", "political"]}, {"long_name": "New South Wales", "short_name": "NSW", "types": ["administrative_area_level_1", "political"]}, {"long_name": "Australia", "short_name": "AU", "types": ["country", "political"]}, {"long_name": "2009", "short_name": "2009", "types": ["postal_code"]}], "geometry": {"location": {"lat": -33.866651, "lng": 151.195827}, "viewport": {"northeast": {"lat": -33.8653881697085, "lng": 151.1969739802915}, "southwest": {"lat": -33.86808613029149, "lng": 151.1942760197085}}}}
console .log (
getAddressValues2 (response)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script><script>
const {applySpec, lift, merge, path, pipe, prop} = R </script>
这个版本将问题分为两部分:一个用于更容易的字段,latitude
一个longitude
用于其他更难匹配的字段,然后简单地合并将每个应用到响应的结果。
更简单的字段不需要评论。这只是一个简单的应用applySpec
和path
。另一个封装为matchType
接受将输入(以及要提取的字段的名称)上的类型匹配到输出的属性名称的规范。它基于类型构建索引desc
(这里使用JSON.stringify
,尽管有明显的替代方案)。然后,它减少一个对象数组,查找任何其types
属性在索引中的对象,并将其值与适当的字段名称联系起来。
这是一个有趣的变体。我仍然更喜欢我原来的,但对于大型阵列,这可能会对性能产生重大影响。
另一个更新
在阅读了 user633183 的回答后,我一直在思考我想如何使用这样的东西。Maybe
在这里使用 s有很多话要说。但是我可能希望通过两种不同的方式与结果进行交互。一个让我可以逐个字段地操作,每个字段都包含在自己的Maybe
. 另一种是作为一个完整的对象,具有它的所有领域;但由于所展示的原因,它必须被包裹在它自己的 Maybe 中。
这是一个不同的版本,它生成第一个变体并包含一个将其转换为第二个变体的函数。
const maybeObj = pipe (
toPairs,
map(([k, v]) => v.isJust ? Just([k, v.value]) : Nothing()),
sequence(Maybe),
map(fromPairs)
)
const maybeSpec = (spec = {}) => (obj = {}) =>
Object .entries (spec) .reduce (
(a, [k, f] ) => ({...a, [k]: Maybe (is (Function, f) && f(obj))}),
{}
)
const findByTypes = (types = []) => (xs = []) =>
xs .find (x => equals (x.types, types) )
const getByTypes = (name) => (types) => pipe (
findByTypes (types),
prop (name)
)
const getAddressComponent = (types) => pipe (
prop ('address_components'),
getByTypes ('long_name') (types)
)
const response = {"address_components": [{"long_name": "5", "short_name": "5", "types": ["floor"]}, {"long_name": "48", "short_name": "48", "types": ["street_number"]}, {"long_name": "Pirrama Road", "short_name": "Pirrama Rd", "types": ["route"]}, {"long_name": "Pyrmont", "short_name": "Pyrmont", "types": ["locality", "political"]}, {"long_name": "Council of the City of Sydney", "short_name": "Sydney", "types": ["administrative_area_level_2", "political"]}, {"long_name": "New South Wales", "short_name": "NSW", "types": ["administrative_area_level_1", "political"]}, {"long_name": "Australia", "short_name": "AU", "types": ["country", "political"]}, {"long_name": "2009", "short_name": "2009", "types": ["postal_code"]}], "geometry": {"location": {"lat": -33.866651, "lng": 151.195827}, "viewport": {"northeast": {"lat": -33.8653881697085, "lng": 151.1969739802915}, "southwest": {"lat": -33.86808613029149, "lng": 151.1942760197085}}}}
getAddressComponent (['route']) (response)
const extractAddress = maybeSpec({
latitude: path (['geometry', 'location', 'lat']),
longitude: path (['geometry', 'location', 'lng']),
city: getAddressComponent (['locality', 'political']),
zipCode: getAddressComponent (['postal_code']),
streetName: getAddressComponent (['route']),
streetNumber: getAddressComponent (['street_number']),
})
const transformed = extractAddress (response)
// const log = pipe (toString, console.log)
const log1 = (obj) => console.log(map(toString, obj))
const log2 = pipe (toString, console.log)
// First variation
log1 (
transformed
)
// Second variation
log2 (
maybeObj (transformed)
)
<script src="https://bundle.run/ramda@0.26.1"></script>
<script src="https://bundle.run/ramda-fantasy@0.8.0"></script>
<script>
const {equals, fromPairs, is, map, path, pipe, prop, toPairs, sequence, toString} = ramda;
const {Maybe} = ramdaFantasy;
const {Just, Nothing} = Maybe;
</script>
该函数maybeObj
转换如下结构:
{
city: Just('Pyrmont'),
latitude: Just(-33.866651)
}
变成这样的:
Just({
city: 'Pyrmont',
latitude: -33.866651
})
但有一个Nothing
:
{
city: Just('Pyrmont'),
latitude: Nothing()
}
回到Nothing
:
Nothing()
它对对象的作用与R.sequence
对数组和其他可折叠类型的作用非常相似。(由于长期复杂的原因,Ramda 不将对象视为可折叠的。)
其余部分很像@user633183 的答案,但用我自己的成语写成。可能唯一值得注意的其他部分是maybeSpec
,它的行为很像,R.applySpec
但将每个字段包装在 aJust
或 aNothing
中。
(请注意,我正在使用Maybe
来自 Ramda-Fantasy的项目。该项目已停止,我可能应该弄清楚使用其中一个最新项目需要进行哪些更改。将其归咎于懒惰。我想,唯一需要改变的就是Maybe
用他们提供的任何函数替换对的调用[或你自己的],以将 nil 值转换为Nothing
和其他所有值转换为Just
s。)
推荐阅读
- javascript - 为什么我不能得到结果?
- java - 如何将 util java 类重用到其他空手道项目中?
- ruby - 在 Ruby 上递归查找层次结构中的目录路径
- java - Maven Sonarqube 插件:无法执行 SonarQube:无法从服务器获取引导索引
- c - 我如何将此结构传递给函数?我有一个模糊的想法,但需要一些建议
- c# - 如何在网格内移动图像?
- android - 代号一:在 Android 设备上使用换行符记录文件内容
- java - 测试时出现空指针异常?
- python - Python,如何找到3维数组的维数?
- unix - 如何验证使用 FTP 提交到 z/OS 的作业是否已完成?