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/.
這是否回答你的問題?
添加回答
舉報