2 回答
TA贡献1802条经验 获得超5个赞
可以在下一个提供的示例代码下方找到对所选方法的非常详细的解释。
const ingredientList = [{
"amount": "1",
"val": "packet pasta"
}, {
"val": "Chicken breast"
}, {
"val": "Ground ginger"
}, {
"amount": "8 cloves",
"val": "garlic, minced"
}, {
"amount": "1",
"val": "onion"
}, {
"amount": "½ tsp",
"val": "paprika"
"amount": "1 Chopped",
"val": "Tomato"
}, {
"amount": "1/2 Cup",
"val": "yogurt"
}, {
"amount": "1/2 teaspoon",
"val": "heavy cream"
}, {
"amount": "½ tsp",
"val": "fine sea salt"
}];
const spiceList = ["paprika", "parsley", "peppermint", "poppy seed", "rosemary"];
const meatList = ["steak", "ground beef", "stewing beef", "roast beef", "ribs", "chicken breast"];
const dairyList = ["milk", "eggs", "egg", "cheese", "yogurt", "cream"];
const produceList = ["peppers", "pepper", "radishes", "radish", "onions", "onion", "Tomatos", "Tomato", "Garlic", "Ginger"];
function groupItemByCategoryDescriptorAndSourceKey(collector, item) {
const {
descriptorList,
uncategorizableKey,
itemSourceKey,
index
} = collector;
const isEqualCategoryValues = (
((typeof collector.isEqualCategoryValues === 'function') && collector.isEqualCategoryValues) ||
((itemValue, categoryValue) => {
// this is the default implementation of how to determine equality
// of two values in case no other function was provided via the
// `collector`'s `isEqualCategoryValues` property.
itemValue = itemValue.trim().replace((/\s+/g), ' ').toLowerCase();
categoryValue = categoryValue.trim().replace((/\s+/g), ' ').toLowerCase();
return (itemValue === categoryValue);
})
);
let currentCategoryList;
function doesBoundValueEqualCategoryValue(categoryValue) {
return isEqualCategoryValues(this.value, categoryValue);
}
function doesBoundValueMatchCategoryAndWhichIsIt(descriptor) {
const isMatchingValue = descriptor.valueList.some(
doesBoundValueEqualCategoryValue, this
);
if (isMatchingValue) { // ... and which is it?
const categoryKey = descriptor.targetKey;
currentCategoryList = (
index[categoryKey] ||
(index[categoryKey] = [])
);
currentCategoryList.push(item);
}
return isMatchingValue;
}
const isCategorizable = descriptorList.some(
doesBoundValueMatchCategoryAndWhichIsIt,
{ value: item[itemSourceKey] }
);
if (!isCategorizable) {
currentCategoryList = (
index[uncategorizableKey] ||
(index[uncategorizableKey] = [])
);
currentCategoryList.push(item);
}
return collector;
}
console.log(
'Shopping List :', JSON.parse(JSON.stringify([ // in order to get rid of SO specific object reference logs.
ingredientList.reduce(groupItemByCategoryDescriptorAndSourceKey, {
descriptorList: [{
targetKey: 'spicesOutput',
valueList: spiceList
}, {
targetKey: 'meatsOutput',
valueList: meatList
}, {
targetKey: 'dairyOutput',
valueList: dairyList
}, {
targetKey: 'produceOutput',
valueList: produceList
}],
uncategorizableKey: 'noCategoryOutput',
// isEqualCategoryValues: anyCustomImplementationWhichDeterminesEqualityOfTwoCategoryValues
itemSourceKey: 'val',
index: {}
}).index]))
);
function isEqualCategoryValues(itemValue, categoryValue) {
// this is a custom implementation of how
// to determine equality of two category.
itemValue = itemValue.trim().replace((/\s+/g), ' ').toLowerCase();
categoryValue = categoryValue.trim().replace((/\s+/g), ' ').toLowerCase();
return (
(itemValue === categoryValue) ||
RegExp('\\b' + categoryValue + '\\b').test(itemValue)
);
}
console.log(
'Shopping List (custom method for equality of category values) :', JSON.parse(JSON.stringify([
ingredientList.reduce(groupItemByCategoryDescriptorAndSourceKey, {
descriptorList: [{
targetKey: 'spicesOutput',
valueList: spiceList
}, {
targetKey: 'meatsOutput',
valueList: meatList
}, {
targetKey: 'dairyOutput',
valueList: dairyList
}, {
targetKey: 'produceOutput',
valueList: produceList
}],
uncategorizableKey: 'noCategoryOutput',
isEqualCategoryValues,
itemSourceKey: 'val',
index: {}
}).index]))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
该方法
OP 提供的问题看起来很像一个(相当复杂的)reduce任务,从成分项目列表到索引/地图,该索引/地图具有成分源列表项目的不同目标列表。
从我的角度来看,将这个 reduce-result 作为唯一的项目推入数组是有问题的。
const shoppingListIndex = {
produceOutput: [{
val: "garlic, minced",
amount: "8 cloves ",
}],
spicesOutput: [{
// ...
}],
NoCategoryOutput: [{
val: "fine sea salt",
amount: "½ tsp",
}]
};
// ... instead of ...
const ShoppingList = [{
produceOutput: [{
// ...
}],
spicesOutput: [{
// ...
}],
NoCategoryOutput: [{
// ...
}]
}];
任何直接的方法都会以某种方式逐步选择一个成分项目,然后再次针对每个项目搜索每个给定的类别列表,直到成分项目的值确实val与当前类别列表中的第一个最佳类别项目匹配。
这个任务可以通过减少功能来概括。为了更加通用,这样的实现不应该对(或不应该“知道”)环境以及所涉及列表的名称和数量等做出任何假设。
因此,这样的实现必须是抽象的和可配置的。这意味着应该清楚如何将 OP 的问题分解为这样的抽象和配置。
reduce 方法accumulator可以用作config对象collector。
因此,为了既不依赖于类别列表的数量也不依赖于它们的名称,确实向collector. 实现将知道/识别此配置项为descriptorList.
此外,为了灵活地命名成分项目的类别目标列表,这样的描述符项目不仅携带可能匹配的类别值列表,而且还具有目标列表名称的属性......
通用 reduce 任务的可能用例可能看起来类似于下一个代码示例......
ingredientList.reduce(groupItemByCategoryDescriptorAndSourceKey, {
descriptorList: [{
targetKey: 'spicesOutput',
valueList: spiceList // the OP's category list example.
}, {
targetKey: 'meatsOutput',
valueList: meatList // the OP's category list example.
}, {
targetKey: 'dairyOutput',
valueList: dairyList // the OP's category list example.
}, {
targetKey: 'produceOutput',
valueList: produceList // the OP's category list example.
}]
});
此外,完全通用的 reduce 任务的配置必须为任何源列表项提供属性名称(键),以便将其值与任何提供的类别值列表中的任何类别值进行比较。实现将知道/识别此配置项为itemSourceKey.
另一个必要的配置项是uncategorizableKey. 它的值将作为无法分类的源列表项的特殊列表的键(意味着在所有提供的类别列表中找不到匹配项)。
将有一个可选的isEqualCategoryValues配置键。如果提供,此属性指的是一个自定义函数,该函数确定两个类别值是否相等;它的第一个itemValue参数保存当前处理的源列表项的引用,第二个categoryValue参数保存当前处理的类别列表的当前处理值的引用。
最后有index一个总是空的对象字面量和 reduce 进程将其结果写入的引用。
因此,通用 reduce 任务的完整用例可能看起来类似于下一个代码示例......
const shoppingListIndex =
ingredientList.reduce(groupItemByCategoryDescriptorAndSourceKey, {
descriptorList: [{
targetKey: 'spicesOutput',
valueList: spiceList
}, {
targetKey: 'meatsOutput',
valueList: meatList
}, {
targetKey: 'dairyOutput',
valueList: dairyList
}, {
targetKey: 'produceOutput',
valueList: produceList
}],
uncategorizableKey: 'noCategoryOutput',
isEqualCategoryValues,
itemSourceKey: 'val',
index: {}
}).index;
比较/确定平等
现在将通用计算部分与案例特定配置分开后,必须关注如何确定两个值的相等性,对于给定的示例,一方面是val成分项的值,另一方面是许多值列在 OP 的类别数组之一中。
例如{ ... "val": "onion" ... }or even { ... "val": "Chicken breast" ... }which 应该在"onion"as ofproduceList和 in "chicken breast"as of 中找到它们相等的对应物meatList。
至于"Chicken breast"vs ,"chicken breast"很明显,比较过程必须将两个操作符都转换为自身的规范化变体。toLowerCase这里已经足够了,但是为了安全起见,应该处理任何空白序列,方法是首先trim输入一个值,然后replace使用单个空白字符 'ing 任何其他剩余的空白序列。
因此,一个已经足够好的平等标准比较可能看起来像......
function isEqualCategoryValues(itemValue, categoryValue) {
itemValue = itemValue.trim().replace((/\s+/g), ' ').toLowerCase();
categoryValue = categoryValue.trim().replace((/\s+/g), ' ').toLowerCase();
return (itemValue === categoryValue);
});
...事实上,这是作为 reducer 函数的内部部分实现的回退,以防没有为 reducer 的收集器/配置对象提供用于确定相等性的自定义函数。
对于任何不太精确的成分和类别值,这种朴素的值相等性检查确实会立即失败,就像示例代码中的那些... "Ground ginger"vs "Ginger"from produceList, ... "heavy cream"vs "cream"from dairyList, ... "garlic, minced"vs "Garlic"again from produceList。
很明显,需要更好的定制平等检查,以完全涵盖 OP 的需求/要求/验收标准。但是,解决问题现在归结为只提供一个定制的函数也很好,它只解决了一个人如何准确地确定价值平等的一部分。
手头有"ground ginger"vs"ginger"的已经规范化的变体,并考虑在字符串值中出现超过 2 个单词的情况,这些单词由空格和/或单词边界(y)ie(s)分隔和/或终止,一个有效的方法可以基于正则表达式 / ( RegExp)
console.log(
"(/\\bginger\\b/).test('ground ginger') ?",
(/\bginger\b/).test('ground ginger')
);
console.log(
"RegExp('\\\\b' + 'ginger' + '\\\\b', 'i').test('ground ginger') ?",
RegExp('\\b' + 'ginger' + '\\b').test('ground ginger')
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
展开片段
isEqualCategoryValues因此,可靠地涵盖 OP 用例的自定义函数的实现几乎与内部使用的默认相等性检查相同。它还具有RegExp基于检查的功能,有时会构建和测试正确的正则表达式,就像本段上方的可执行示例代码所演示的那样。
完整的自定义实现可能看起来像那样......
function isEqualCategoryValues(itemValue, categoryValue) {
itemValue = itemValue.trim().replace((/\s+/g), ' ').toLowerCase();
categoryValue = categoryValue.trim().replace((/\s+/g), ' ').toLowerCase();
return (
(itemValue === categoryValue) ||
RegExp('\\b' + categoryValue + '\\b').test(itemValue)
);
}
Reduce 逻辑/实现
已经了解了原因(通用的 reduce 任务但配置灵活,因此能够处理各种各样的用例)以及如何使用 reduce 函数收集器配置......
const shoppingListIndex =
ingredientList.reduce(groupItemByCategoryDescriptorAndSourceKey, {
descriptorList: [{ /* ... */ }, { /* ... */ }/*, ... */],
uncategorizableKey: 'noCategoryOutput',
isEqualCategoryValues,
itemSourceKey: 'val',
index: {}
}).index;
...现在可以通过字面上遵循上面的“方法”部分中的文字来继续 reduce 逻辑的实际实现。
再次阅读本节,可能会形成一个完全由堆叠some任务构建的解决方案。的本质是用第一个找到的匹配项(布尔返回值)some尽快离开搜索任务(打破迭代循环) 。true这正是解决 OP 问题需要做的事情;并且堆叠是由于搜索应该在类别值列表列表中找到其匹配项的值。
由于基于方法的检测功能some不仅要确保“提前退出”,而且还需要提供有关第二个比较值的信息,因此必须使用回调函数的this上下文作为数据载体。
最外层的some基础检测方法解决了编写/收集找到的类别的额外任务。因此这个方法可以被命名doesBoundValueMatchCategoryAndWhichIsIt并且它的用法很可能看起来像下一个代码示例......
// iterate the (descriptor) list of category lists.
const isCategorizable = descriptorList.some(
doesBoundValueMatchCategoryAndWhichIsIt,
{ value: item[itemSourceKey] }
);
可以看出,整个some堆栈的最终返回值是否表明(成分)值是否可以分类(或不分类)。
的实现doesBoundValueMatchCategoryAndWhichIsIt可能看起来类似于这个......
function doesBoundValueMatchCategoryAndWhichIsIt(descriptor) {
// iterate the current category list.
// boolean return value
const isMatchingValue = descriptor.valueList.some(
doesBoundValueEqualCategoryValue, this
);
// act upon the return value.
//
// - push the item of the related value- match
// into the corresponding category list (create
// the latter in case it did not yet exist).
if (isMatchingValue) { // ... and which is it?
const categoryKey = descriptor.targetKey;
currentCategoryList = (
index[categoryKey] ||
(index[categoryKey] = [])
);
currentCategoryList.push(item);
}
// forces "early exit" in case of being `true`.
return isMatchingValue;
}
随着doesBoundValueEqualCategoryValue当前处理的(成分)项目价值的通过几乎已经结束。此函数将其绑定的当前项目值及其第一个参数(当前类别值)转发给相等函数(后者作为自定义变体或内部默认值提供)...
function doesBoundValueEqualCategoryValue(categoryValue) {
return isEqualCategoryValues(this.value, categoryValue);
}
最后,如果无法对当前处理的(成分)项目值进行分类,则该项目将被推入由 collectors 属性标识的列表中uncategorizableKey。
就是这样。谢谢阅读。
奖金(自以为是)
考虑到OP的另一个相关问题......如何最好地解析成分列表中的每一项并根据每个解析结果创建一个新对象?...以及那里的一种方法...其中一种方法非常强大,例如下一个基于可配置的reduce流程链...
const ingredientList = [
'1 packet pasta',
'Chicken breast',
'Ground ginger',
'8 cloves garlic, minced',
'1 onion',
'½ tsp paprika',
'1 Chopped Tomato',
'1/2 Cup yogurt',
'1/2 teaspoon heavy cream',
'½ tsp fine sea salt'
];
const measuringUnitList = [
'tbsp', 'tablespoons', 'tablespoon', 'tsp', 'teaspoons', 'teaspoon', 'chopped',
'oz', 'ounces', 'ounce', 'fl. oz', 'fl. ounces', 'fl. ounce', 'fluid ounces', 'fluid ounce',
'cups', 'cup', 'qt', 'quarts', 'quart', 'pt', 'pints', 'pint', 'gal', 'gallons', 'gallon',
'ml', 'milliliter', 'l', 'liter',
'g', 'gram', 'kg', 'kilogram'
];
const spiceList = ["paprika", "parsley", "peppermint", "poppy seed", "rosemary"];
const meatList = ["steak", "ground beef", "stewing beef", "roast beef", "ribs", "chicken breast"];
const dairyList = ["milk", "eggs", "egg", "cheese", "yogurt", "cream"];
const produceList = ["peppers", "pepper", "radishes", "radish", "onions", "onion", "Tomatos", "Tomato", "Garlic", "Ginger"];
function isEqualCategoryValues(itemValue, categoryValue) {
itemValue = itemValue.trim().replace((/\s+/g), ' ').toLowerCase();
categoryValue = categoryValue.trim().replace((/\s+/g), ' ').toLowerCase();
return (
(itemValue === categoryValue) ||
RegExp('\\b' + categoryValue + '\\b').test(itemValue)
);
}
console.log('Ingredient List :', ingredientList);
console.log(
'Shopping List Index :', JSON.parse(JSON.stringify( // in order to get rid of SO specific object reference logs.
ingredientList.reduce(collectNamedCaptureGroupData, {
regXPrimary: createUnitCentricCapturingRegX(measuringUnitList),
regXSecondary: unitlessCapturingRegX,
defaultKey: 'val',
list: []
}).list.reduce(groupItemByCategoryDescriptorAndSourceKey, {
descriptorList: [{
targetKey: 'spicesOutput',
valueList: spiceList
}, {
targetKey: 'meatsOutput',
valueList: meatList
}, {
targetKey: 'dairyOutput',
valueList: dairyList
}, {
targetKey: 'produceOutput',
valueList: produceList
}],
uncategorizableKey: 'noCategoryOutput',
isEqualCategoryValues,
itemSourceKey: 'val',
index: {}
}).index))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// [https://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript/9310752#9310752]
function escapeRegExpSearchString(text) {
// return text.replace(/[-[\]{}()*+?.,\\^$|#\\s]/g, '\\$&');
// ... slightly changed ...
return text
.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
.replace((/\s+/), '\\s+');
}
// https://stackoverflow.com/questions/63880334/how-does-one-parse-best-each-item-of-an-ingredient-list-and-does-create-a-new-ob/63885323#63885323
function createUnitCentricCapturingRegX(unitList) {
// see: [https://regex101.com/r/7bmGXN/1/]
// e.g. (/^(?<amount>.*?)\s*\b(?<unit>tsp|...|fl\.\s*ounces|fl\.\s*ounce|cup)\b\s*(?<content>.*)$/)
const options = unitList
.map(unit => escapeRegExpSearchString(unit))
.join('|')
.replace((/\\\.\\s\+/g), '\\\.\\s*');
return RegExp('^(?<amount>.*?\\s*\\b(?:' + options + '))\\b\\s*(?<val>.*)$', 'i');
}
const unitlessCapturingRegX = (/^(?<amount>¼|½|¾|\d+\/\d+|\d+)\s*(?<val>.*)$/);
function collectNamedCaptureGroupData(collector, item) {
item = item.trim();
const { regXPrimary, regXSecondary, defaultKey, list } = collector;
const result = regXPrimary.exec(item) || regXSecondary.exec(item);
list.push(
(result && result.groups && Object.assign({}, result.groups))
|| { [defaultKey]: item }
);
return collector;
}
// https://stackoverflow.com/questions/63884077/how-does-one-categorize-a-list-of-data-items-via-many-different-category-lists-w/63907980#63907980
function groupItemByCategoryDescriptorAndSourceKey(collector, item) {
const {
descriptorList,
uncategorizableKey,
itemSourceKey,
index
} = collector;
const isEqualCategoryValues = (
((typeof collector.isEqualCategoryValues === 'function') && collector.isEqualCategoryValues) ||
((itemValue, categoryValue) => {
// this is the default implementation of how to determine equality
// of two values in case no other function was provided via the
// `collector`'s `isEqualCategoryValues` property.
itemValue = itemValue.trim().replace((/\s+/g), ' ').toLowerCase();
categoryValue = categoryValue.trim().replace((/\s+/g), ' ').toLowerCase();
return (itemValue === categoryValue);
})
);
let currentCategoryList;
function doesBoundValueEqualCategoryValue(categoryValue) {
return isEqualCategoryValues(this.value, categoryValue);
}
function doesBoundValueMatchCategoryAndWhichIsIt(descriptor) {
const isMatchingValue = descriptor.valueList.some(
doesBoundValueEqualCategoryValue, this
);
if (isMatchingValue) { // ... and which is it?
const categoryKey = descriptor.targetKey;
currentCategoryList = (
index[categoryKey] ||
(index[categoryKey] = [])
);
currentCategoryList.push(item);
}
return isMatchingValue;
}
const isCategorizable = descriptorList.some(
doesBoundValueMatchCategoryAndWhichIsIt,
{ value: item[itemSourceKey] }
);
if (!isCategorizable) {
currentCategoryList = (
index[uncategorizableKey] ||
(index[uncategorizableKey] = [])
);
currentCategoryList.push(item);
}
return collector;
}
</script>
展开片段
TA贡献1784条经验 获得超9个赞
您可以将搜索数组更改为带有i不区分大小写搜索标志的正则表达式,并将成分转换val为两边都带有通配符的正则表达式(如果它们是复数或有其他信息):
const Ingris = [
{
val: "onion,",
amount: "1",
},
{
val: "paprika",
amount: "½ tsp",
},
{
val: "yogurt",
amount: "1/2 Cup",
},
{
val: "fine sea salt",
amount: "½ tsp ",
},
];
var spices = [/paprika/i, /parsley/i, /peppermint/i, /poppy seed/i, /rosemary/i];
var meats = [/steak/i, /ground beef/i, /stewing beef/i, /roast beef/i, /ribs/i, /chicken/i];
var dairy = [/milk/i, /egg/i, /cheese/i, /yogurt/i];
var produce = [/pepper/i, /radish/i, /onion/i, /Tomato/i];
function shoppingList(array, ingredient) {
for (var i = 0; i < array.length; i++) {
if (ingredient.match(array[i])) {
return ingredient;
}
}
}
function Categorize() {
let produceOutput = [];
let NoCategoryOutput = [];
for (const [key, value] of Object.entries(Ingris)) {
var ingredient = '/\.*' + value.val + '\.*/';
if (shoppingList(spices, ingredient) || shoppingList(meats, ingredient) || shoppingList(dairy, ingredient) || shoppingList(produce, ingredient)) {
produceOutput.push(value);
} else {
NoCategoryOutput.push(value);
}
}
var ShoppingList = new Object();
ShoppingList.produceOutput = produceOutput;
ShoppingList.NoCategoryOutput = NoCategoryOutput;
console.log(ShoppingList);
}
Categorize();
如果您希望这对复数和单数成分都有效,则必须确保搜索数组值都是单数(即,"onions"您需要使用/onion/.
这是否回答你的问题?
添加回答
举报