การแบ่งอาร์เรย์ด้วยฟังก์ชันตัวกรอง


92

ฉันมีอาร์เรย์ Javascript ที่ฉันต้องการแบ่งออกเป็นสองรายการโดยขึ้นอยู่กับว่าฟังก์ชันที่เรียกในแต่ละองค์ประกอบจะส่งกลับtrueหรือfalse. โดยพื้นฐานแล้วนี่คือarray.filterแต่ฉันก็อยากจะมีองค์ประกอบที่ถูกกรองออกไปด้วย

ปัจจุบันแผนของฉันคือการใช้array.forEachและเรียกใช้ฟังก์ชันเพรดิเคตในแต่ละองค์ประกอบ ขึ้นอยู่กับว่านี่เป็นจริงหรือเท็จฉันจะผลักองค์ประกอบปัจจุบันไปยังหนึ่งในสองอาร์เรย์ใหม่ มีวิธีที่หรูหรากว่านี้หรือดีกว่านี้ไหม? array.filterที่จะผลักดันองค์ประกอบบนอาร์เรย์อื่นก่อนที่จะส่งกลับfalseเช่น?


หากคุณสามารถโพสต์โค้ดตัวอย่างได้จะช่วยให้คำตอบที่ดีขึ้น!
Mark Pieszak - Trilon.io

ไม่ว่าคุณจะใช้ Javascript แบบใดก็ตามจะต้อง: วนซ้ำรายการเรียกใช้ฟังก์ชันพุชรายการลงในอาร์เรย์ ฉันไม่คิดว่าจะมีวิธีใดที่จะทำให้มีประสิทธิภาพมากขึ้น
TheZ

3
คุณสามารถทำอะไรก็ได้ที่ต้องการในการเรียกกลับที่ส่งผ่านไป.filterแต่ผลข้างเคียงดังกล่าวยากที่จะติดตามและทำความเข้าใจ เพียงแค่วนซ้ำบนอาร์เรย์และดันไปยังอาร์เรย์หนึ่งหรืออีกอาร์เรย์
Felix Kling

คำตอบ:


76

ด้วย ES6 คุณสามารถใช้ประโยชน์จากรูปแบบการแพร่กระจายด้วยการลด:

function partition(array, isValid) {
  return array.reduce(([pass, fail], elem) => {
    return isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]];
  }, [[], []]);
}

const [pass, fail] = partition(myArray, (e) => e > 5);

หรือในบรรทัดเดียว:

const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);

8
สำหรับฉันพาร์ทิชัน lodash หรือเพียงแค่ forEach จะเข้าใจง่ายกว่า แต่ถึงอย่างนั้นความพยายามที่ดี
Toni Leigh

15
สิ่งนี้จะสร้างอาร์เรย์ใหม่สองอาร์เรย์สำหรับทุกองค์ประกอบในต้นฉบับ แม้ว่าอาร์เรย์หนึ่งจะมีเพียงสององค์ประกอบ แต่อีกอาร์เรย์จะเติบโตตามขนาดของอาร์เรย์ สิ่งนี้จะช้ามากและเปลืองหน่วยความจำมาก (คุณสามารถทำได้ด้วยการผลักดันและมันจะมีประสิทธิภาพมากขึ้น)
Stuart Schechter

ขอบคุณช่วยเวลาของฉัน!
7urkm3n

นี่เป็นประโยชน์จริงๆ braza หมายเหตุสำหรับผู้อื่น: มีเวอร์ชันที่อ่านได้เพิ่มเติมด้านล่าง
Raffi

38

คุณสามารถใช้lodash.partition

var users = [
  { 'user': 'barney',  'age': 36, 'active': false },
  { 'user': 'fred',    'age': 40, 'active': true },
  { 'user': 'pebbles', 'age': 1,  'active': false }
];

_.partition(users, function(o) { return o.active; });
// → objects for [['fred'], ['barney', 'pebbles']]

// The `_.matches` iteratee shorthand.
_.partition(users, { 'age': 1, 'active': false });
// → objects for [['pebbles'], ['barney', 'fred']]

// The `_.matchesProperty` iteratee shorthand.
_.partition(users, ['active', false]);
// → objects for [['barney', 'pebbles'], ['fred']]

// The `_.property` iteratee shorthand.
_.partition(users, 'active');
// → objects for [['fred'], ['barney', 'pebbles']]

หรือramda.partition

R.partition(R.contains('s'), ['sss', 'ttt', 'foo', 'bars']);
// => [ [ 'sss', 'bars' ],  [ 'ttt', 'foo' ] ]

R.partition(R.contains('s'), { a: 'sss', b: 'ttt', foo: 'bars' });
// => [ { a: 'sss', foo: 'bars' }, { b: 'ttt' }  ]

16

คุณสามารถใช้ลดได้:

function partition(array, callback){
  return array.reduce(function(result, element, i) {
    callback(element, i, array) 
      ? result[0].push(element) 
      : result[1].push(element);

        return result;
      }, [[],[]]
    );
 };

อัปเดต. การใช้ไวยากรณ์ ES6 คุณสามารถทำได้โดยใช้การเรียกซ้ำ:

function partition([current, ...tail], f, [left, right] = [[], []]) {
    if(current === undefined) {
        return [left, right];
    }
    if(f(current)) {
        return partition(tail, f, [[...left, current], right]);
    }
    return partition(tail, f, [left, [...right, current]]);
}

3
IMHO โซลูชันแรก (push) คือประสิทธิภาพที่ดีขึ้นเมื่อขนาดอาร์เรย์เติบโตขึ้น
ToolmakerSteve

@ToolmakerSteve คุณสามารถอธิบายเพิ่มเติมได้ว่าทำไม? ฉันยังอ่านความคิดเห็นเกี่ยวกับคำตอบยอดนิยม แต่ก็ยังงงว่าทำไม
buncis

2
@buncis. แนวทางแรกตรวจสอบแต่ละองค์ประกอบครั้งเดียวเพียงแค่ผลักดันองค์ประกอบนั้นไปยังอาร์เรย์ที่เหมาะสม แนวทางที่สองสร้าง[...left, current]หรือ[...right, current]- สำหรับแต่ละองค์ประกอบ ฉันไม่รู้ภายในที่แน่นอน แต่ฉันแน่ใจว่าการก่อสร้างมีราคาแพงกว่าการผลักองค์ประกอบไปยังอาร์เรย์ นอกจากนี้ตามกฎทั่วไปแล้วการเรียกซ้ำจะมีราคาแพงกว่าการวนซ้ำเนื่องจากการสร้าง "สแต็กเฟรม" ในแต่ละครั้ง
ToolmakerSteve

14

ฟังดูคล้ายกับวิธีการของทับทิมEnumerable#partitionมาก

หากฟังก์ชันไม่สามารถมีผลข้างเคียงได้ (กล่าวคือไม่สามารถเปลี่ยนอาร์เรย์เดิมได้) แสดงว่าไม่มีวิธีใดที่มีประสิทธิภาพในการแบ่งพาร์ติชันอาร์เรย์ได้ดีไปกว่าการวนซ้ำในแต่ละองค์ประกอบและผลักองค์ประกอบไปยังหนึ่งในสองอาร์เรย์ของคุณ

ตามที่กล่าวไว้มันเป็นเนื้อหาที่ "สง่างาม" มากกว่าในการสร้างวิธีการArrayเพื่อทำหน้าที่นี้ ในตัวอย่างนี้ฟังก์ชันตัวกรองจะทำงานในบริบทของอาร์เรย์ดั้งเดิม (กล่าวคือthisจะเป็นอาร์เรย์ดั้งเดิม) และได้รับองค์ประกอบและดัชนีขององค์ประกอบเป็นอาร์กิวเมนต์ (คล้ายกับวิธีของ jQueryeach ):

Array.prototype.partition = function (f){
  var matched = [],
      unmatched = [],
      i = 0,
      j = this.length;

  for (; i < j; i++){
    (f.call(this, this[i], i) ? matched : unmatched).push(this[i]);
  }

  return [matched, unmatched];
};

console.log([1, 2, 3, 4, 5].partition(function (n, i){
  return n % 2 == 0;
}));

//=> [ [ 2, 4 ], [ 1, 3, 5 ] ]

11
สำหรับผู้อ่านสมัยใหม่โปรดอย่าเพิ่มวิธีการให้กับวัตถุไลบรารีมาตรฐานสากล มันอันตรายและมีแนวโน้มที่จะถูกเขียนทับซึ่งนำไปสู่พฤติกรรมที่ลึกลับและเสียประโยชน์ ฟังก์ชันเก่าธรรมดาที่กำหนดขอบเขตอย่างถูกต้องปลอดภัยกว่ามากและการเรียก myFunc (อาร์เรย์) ก็ไม่น้อยไปกว่า array.myFunc ()
Emmett R.

14

ฉันมากับผู้ชายตัวเล็ก ๆ คนนี้ มันใช้สำหรับแต่ละสิ่งที่คุณอธิบาย แต่มันดูสะอาดและรวบรัดในความคิดของฉัน

//Partition function
function partition(array, filter) {
  let pass = [], fail = [];
  array.forEach((e, idx, arr) => (filter(e, idx, arr) ? pass : fail).push(e));
  return [pass, fail];
}

//Run it with some dummy data and filter
const [lessThan5, greaterThanEqual5] = partition([0,1,4,3,5,7,9,2,4,6,8,9,0,1,2,4,6], e => e < 5);

//Output
console.log(lessThan5);
console.log(greaterThanEqual5);


1
วิธีนี้ดีกว่าวิธีการบางอย่างที่มีคะแนนโหวตมากกว่า IMHO อ่านง่ายสร้างการส่งผ่านอาร์เรย์เพียงครั้งเดียวและไม่จัดสรรและจัดสรรอาร์เรย์ผลลัพธ์พาร์ติชันใหม่ ฉันชอบที่มันแสดงค่าตัวกรองทั่วไปทั้งสามค่าให้กับฟังก์ชันตัวกรอง (ค่าดัชนีและอาร์เรย์ทั้งหมด) ซึ่งจะทำให้ฟังก์ชันนี้สามารถใช้ซ้ำได้มากขึ้น
speckledcarp

ฉันชอบอันนี้มากเพราะเรียบง่ายและสง่างาม ต้องบอกว่าฉันพบว่ามันช้ากว่าforลูปธรรมดาอย่างมากเหมือนในคำตอบเก่าของ @qwertymk เช่นสำหรับอาร์เรย์ที่มีองค์ประกอบ 100,000 รายการระบบของฉันช้าเป็นสองเท่า
tromgy

9

ในฟังก์ชันตัวกรองคุณสามารถพุชรายการเท็จของคุณไปยังฟังก์ชันภายนอกตัวแปรอื่น:

var bad = [], good = [1,2,3,4,5];
good = good.filter(function (value) { if (value === false) { bad.push(value) } else { return true});

แน่นอนว่าvalue === falseต้องเปรียบเทียบจริงๆ;)

forEachแต่ก็ดำเนินการเกือบทุกอย่างที่เหมือนกันเช่น ฉันคิดว่าคุณควรใช้forEachเพื่อให้อ่านโค้ดได้ดีขึ้น


เห็นด้วยกับโปสเตอร์ด้านบน ... การใส่ตรรกะแบบนี้ในฟังก์ชั่นตัวกรองทำให้รู้สึกป่องและจัดการได้ยาก
theUtherSide

ฉันคิดว่าตัวกรองมีเหตุผลคุณต้องการลบออกจากอาร์เรย์เดิมดังนั้นจึงเป็นตัวกรอง
Mojimi

5

อ่านง่าย

const partition = (arr, condition) => {
    const trues = arr.filter(el => condition(el));
    const falses = arr.filter(el => !condition(el));
    return [trues, falses];
};

// sample usage
const nums = [1,2,3,4,5,6,7]
const [evens, odds] = partition(nums, (el) => el%2 == 0)

6
ข้อเสียคือคุณสร้าง 2 ลูปแทนเพียงอันเดียว
Laurent

โหวตเพิ่มความสามารถในการอ่านสำหรับอาร์เรย์ขนาดเล็กอันนี้ชัดเจนกว่าหลักฐานในอนาคต
allan.simon

4

ลองสิ่งนี้:

function filter(a, fun) {
    var ret = { good: [], bad: [] };
    for (var i = 0; i < a.length; i++)
        if (fun(a[i])
            ret.good.push(a[i]);
        else
            ret.bad.push(a[i]);
    return ret;
}

การสาธิต


ชี้ให้เห็นว่าฟังก์ชั่นตัวกรองของอาร์เรย์ไม่ได้รับการสนับสนุนในทุกเบราว์เซอร์ (ฉันกำลังมองหา IE ที่เก่ากว่า)
TheZ

4

อะไรประมาณนี้

[1,4,3,5,3,2].reduce( (s, x) => { s[ x > 3 ].push(x); return s;} , {true: [], false:[]} )

อาจมีประสิทธิภาพมากกว่าตัวดำเนินการสเปรด

หรือสั้นกว่าเล็กน้อย แต่น่าเกลียดกว่า

[1,4,3,5,3,2].reduce( (s, x) => s[ x > 3 ].push(x)?s:s , {true: [], false:[]} )


2

คำตอบจำนวนมากที่นี่ใช้Array.prototype.reduceในการสร้างตัวสะสมที่เปลี่ยนแปลงได้และชี้ให้เห็นอย่างถูกต้องว่าสำหรับอาร์เรย์ขนาดใหญ่สิ่งนี้มีประสิทธิภาพมากกว่าการใช้ตัวดำเนินการกระจายเพื่อคัดลอกอาร์เรย์ใหม่ในแต่ละการวนซ้ำ ข้อเสียคือมันไม่สวยเท่านิพจน์ "บริสุทธิ์" ที่ใช้ไวยากรณ์แลมบ์ดาแบบสั้น

แต่วิธีแก้ปัญหานั้นคือการใช้ตัวดำเนินการลูกน้ำ ในภาษาคล้าย C เครื่องหมายจุลภาคเป็นตัวดำเนินการที่ส่งกลับตัวถูกดำเนินการทางขวามือเสมอ คุณสามารถใช้สิ่งนี้เพื่อสร้างนิพจน์ที่เรียกใช้ฟังก์ชันโมฆะและส่งคืนค่า

function partition(array, predicate) {
    return array.reduce((acc, item) => predicate(item)
        ? (acc[0].push(item), acc)
        : (acc[1].push(item), acc), [[], []]);
}

หากคุณใช้ประโยชน์จากข้อเท็จจริงที่ว่านิพจน์บูลีนแปลงเป็นตัวเลขเป็น 0 และ 1 โดยปริยายและคุณสามารถทำให้กระชับมากขึ้นแม้ว่าฉันจะไม่คิดว่ามันอ่านได้:

function partition(array, predicate) {
    return array.reduce((acc, item) => (acc[+!predicate(item)].push(item), acc), [[], []]);
}

การใช้งาน:

const [trues, falses] = partition(['aardvark', 'cat', 'apple'], i => i.startsWith('a'));
console.log(trues); // ['aardvark', 'apple']
console.log(falses); // ['cat']

0

พาร์ติชั่น ONE-LINER

const partition = (a,f)=>a.reduce((p,q)=>(p[+!f(q)].push(q),p),[[],[]]);

การสาธิต

// to make it consistent to filter pass index and array as arguments
const partition = (a, f) =>
    a.reduce((p, q, i, ar) => (p[+!f(q, i, ar)].push(q), p), [[], []]);

console.log(partition([1, 2, 3, 4, 5], x => x % 2 === 0));
console.log(partition([..."ABCD"], (x, i) => i % 2 === 0));

สำหรับ typescript

const partition = <T>(
  a: T[],
  f: (v: T, i?: number, ar?: T[]) => boolean
): [T[], T[]] =>
  a.reduce((p, q, i, ar) => (p[+!f(q, i, ar)].push(q), p), [[], []]);

เวอร์ชัน typescript แสดงข้อผิดพลาดของโค้ดสำหรับ.pushพารามิเตอร์q: "อาร์กิวเมนต์ประเภท 'T' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'never'.ts (2345)"
Steve Goossens

-1

ฉันลงเอยด้วยการทำสิ่งนี้เพราะมันเข้าใจง่าย (และพิมพ์ด้วย typescript)

const partition = <T>(array: T[], isValid: (element: T) => boolean): [T[], T[]] => {
  const pass: T[] = []
  const fail: T[] = []
  array.forEach(element => {
    if (isValid(element)) {
      pass.push(element)
    } else {
      fail.push(element)
    }
  })
  return [pass, fail]
}

// usage
const [pass, fail] = partition([1, 2, 3, 4, 5], (element: number) => element > 3)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.