diff --git a/README.md b/README.md
index a1abe69..85c8946 100644
--- a/README.md
+++ b/README.md
@@ -1,171 +1,157 @@
-:information_source: _Utility classes from this repository have been moved to [apex-utils](https://github.com/ipavlic/apex-utils) repository._
-
# Lambda
-## Functionality
-
-- [`Filter`](#filter)
-- [`GroupBy`](#group-by)
-- [`Pick`](#pick)
-- [`Pluck`](#pluck)
-- [Important notes on the type system in Apex](#type-system)
+Lambda allows functional constructs to be used with `SObject` collections through providing a class named `Collection`.
+A `Collection` is a view of the backing `SObject` collection, and is built from standard Apex collection classes:
-### `Filter`
-
+```java
+List accounts = new List{
+ new Account(Name = 'Foo', AnnualRevenue = 1000),
+ new Account(Name = 'Bar', AnnualRevenue = 5000)
+}
+Collection accountCollection = Collection.of(accounts);
+```
-`Filter` enables filtering lists of sObject records by declaring *criteria* that records have to match through a fluent interface.
+`Collection` instance then offers functional methods like `filter` or `remove`:
-| Modifier and type | Method | Description |
-|-------------------|--------|-------------|
-| `static RecordMatchingFilterQuery` | `match(SObject prototype)` | Constructs and returns an object matching query against the `prototype` |
-| `static PartialFieldFilterQuery` | `field(Schema.SObjectField field)` | Constructs and returns a field matching filter starting with `field` |
-| `static PartialFieldFilterQuery` | `field(String fieldRelation)` | Constructs and returns a field matching filter starting with `fieldRelation` |
+```java
+Collection filtered = accountCollection.filter(Match.field(Account.AnnualRevenue).greaterThan(1000));
+Collection remaining = accountCollection.remove(Match.field(Account.Name).equals('Foo'));
+```
-#### Record matching filter
+Methods which deal with collections return `Collection` views again. Standard Apex collection instances obtained from views
+through `asList()` and `asSet()` methods.
```java
-Account prototype = new Account(
- Name = 'Test',
- AnnualRevenue = 50000000
-);
-// Accounts named 'Test' with an AnnualRevenue of **exactly** 50,000,000 are matched
-List filtered = Filter.match(prototype).apply(accounts);
+List rawList = accountCollection.asList();
+Set rawSet = accountCollection.asSet();
```
-Matches list records against a “prototype” record. A list record is a match if all the fields which are defined on the prototype record are equal to those on the list record.
+## `Collection` functions
-`Filter.match(SObject prototype)` returns a `RecordMatchingFilterQuery` which provides methods to match the filter against a list.
+- [`filter`](#filter)
+- [`remove`](#remove)
+- [`groupBy`](#group-by)
+- [`pick`](#pick)
+- [`pluck`](#pluck)
+- [`mapAll`](#map-all)
+- [`mapSome`](#map-some)
+
+### `filter`
+
| Modifier and type | Method | Description |
|-------------------|--------|-------------|
-| `List` | `apply(Iterable records)` | Matches elements in `records` and returns them as a new list |
-| `List` | `apply(Iterable records, Type listType)` | Matches elements in `records` and returns them as a new list of `listType` type |
-| `FilterResult` | `applyLazy(Iterable records)` | Returns `FilterResult` iterable which can be used for lazy matching to allow extraction of partial results from large sources |
-| `List` | `extract(Iterable records)` | Matches elements in `records`, removes them from the original list and returns them in a new list |
-| `List` | `extract(Iterable records)` | Matches elements in `records`, removes them from the original list and returns them in a new list of `listType` type |
+| `Collection` | `filter(SObjectPredicate predicate)` | Returns a `Collection` view of records that satisfied predicate |
+
+Two predicates are provided out of the box, `FieldsMatch` and `RecordMatch`. They are instantiated through factory methods on `Match`:
-#### Field matching filter
+```java
+Collection accountCollection = Collection.of(accounts);
-Matches against field criteria.
+Account prototype = new Account(Name = 'Foo');
+Collection recordMatched = accountCollection.filter(Match.record(prototype));
+
+Collection fieldMatched = accountCollection.filter(Match.field(Account.Name).equals('Foo'));
+```
+
+#### `FieldsMatch`
+
+`FieldsMatch` returns `true` if a record satisfies all field matching conditions.
+
+`FieldsMatch` is constructed with a fluent interface. `Match` factory method `field` returns an `IncompleteFieldsMatch`.
+`FieldsMatch` is obtained from the `IncompleteFieldsMatch` by providing a matching condition on the field. `FieldsMatch`
+can be expanded with a new matching condition to get another `IncompleteFieldsMatch`. The process is continued until all
+desired matching conditions are defined.
```java
-// Accounts named 'Test' are matched
-List testAccounts = Filter.field(Account.Name).equals('Test').apply(accounts);
+FieldsMatch m = Match.field(Account.Name).equals('Foo').also(Account.AnnualRevenue).greaterThan(100000);
```
-Multiple criteria can be stringed together with `also` (alias `field`) to form the full matching query. Records have to match *all* criteria.
+`FieldsMatch` can be provided directly to `filter` method:
```java
-// Accounts named 'Test' with annual revenue under 100,000 are matched
-List filtered = Filter.field(Account.Name).equals('Test')
- .also(Account.AnnualRevenue).lessThanOrEquals(100000)
- .apply(accounts);
+Collection filtered = Collection.of(accounts).filter(Match.field(Account.Name).equals('Foo').also(Account.AnnualRevenue).greaterThan(100000));
```
-`Filter.field(Schema.SObjectField field)` returns a `PartialFieldFilterQuery` which is used to define criteria:
+##### `IncompleteFieldsMatch`
| Modifier and type | Method | Alias | Description |
|-------------------|--------|-------|-------------|
-| `FieldFilterQuery` | `equals(Object value)` | `eq` | Defines an equality comparison criterium for the current field |
-| `FieldFilterQuery` | `notEquals(Object value)` | `neq` | Defines an inequality comparison criterium for the current field |
-| `FieldFilterQuery` | `lessThan(Object value)` | `lt` | Defines a less than comparison criterium for the current field |
-| `FieldFilterQuery` | `lessThanOrEquals(Object value)` | `leq` | Defines a less than or equals criterium for the current field |
-| `FieldFilterQuery` | `greaterThan(Object value)` | `gt` | Defines a greater than criterium for the current field |
-| `FieldFilterQuery` | `greaterThanOrEquals(Object value)` | `geq` | Defines a greaterThanOrEquals criterium for the current field |
-| `FieldFilterQuery` | `isIn(Object value)` | | Defines a set membership criterium for the current field |
-| `FieldFilterQuery` | `isNotIn(Object value)` | `notIn` | Defines a set non-membership criterium for the current field |
-| `FieldFilterQuery` | `hasValue()` | `notNull` | Defines a non-null criterium for the current field |
-
-`FieldFilterQuery` can then be *applied* to a list, or further criteria can be chained with `also` (alias `field`):
+| `FieldsMatch` | `equals(Object value)` | `eq` | Defines an equality comparison condition for the current field |
+| `FieldsMatch` | `notEquals(Object value)` | `neq` | Defines an inequality comparison condition for the current field |
+| `FieldsMatch` | `lessThan(Object value)` | `lt` | Defines a less than comparison condition for the current field |
+| `FieldsMatch` | `lessThanOrEquals(Object value)` | `leq` | Defines a less than or equals condition for the current field |
+| `FieldsMatch` | `greaterThan(Object value)` | `gt` | Defines a greater than condition for the current field |
+| `FieldsMatch` | `greaterThanOrEquals(Object value)` | `geq` | Defines a greaterThanOrEquals condition for the current field |
+| `FieldsMatch` | `isIn(Object value)` | | Defines a set membership condition for the current field |
+| `FieldsMatch` | `isNotIn(Object value)` | `notIn` | Defines a set non-membership condition for the current field |
+| `FieldsMatch` | `hasValue()` | `notNull` | Defines a non-null condition for the current field |
+
+##### `FieldsMatch`
+
+Additional conditions can be defined with `also`, or its alias, `field`:
| Modifier and type | Method | Alias | Description |
|-------------------|--------|-------|-------------|
-| `PartialFieldFilterQuery` | `also(Schema.SObjectField field)` | `field` | Chains another criterium to the filtering query |
-| `PartialFieldFilterQuery` | `also(String fieldRelationfield)` | `field` | Chains another criterium to the filtering query |
-| `List` | `apply(Iterable records)` | | Matches elements in `records` and returns them as a new list |
-| `List` | `apply(Iterable records, Type listType)` | | Matches elements in `records` and returns them as a new list of `listType` type |
-| `FilterResult` | `applyLazy(Iterable records)` | | Returns `FilterResult` iterable which can be used for lazy matching to allow extraction of partial results from large sources |
-| `List` | `extract(Iterable records)` | | Matches elements in `records`, removes them from the original list and returns them in a new list |
-| `List` | `extract(Iterable records)` | | Matches elements in `records`, removes them from the original list and returns them in a new list of `listType` type |
+| `IncompleteFieldsMatch` | `also(Schema.SObjectField field)` | `field` | Defines another condition to match |
+| `IncompleteFieldsMatch` | `also(String fieldPath)` | `field` | Defines another condition to match |
-#### Warning :warning:
+##### Warning :warning:
-Most criteria expect a primitive value to compare against. `isIn` and `isNotIn` instead expect a `Set` of one of the following types: `Boolean`, `Date`, `Decimal`, `Double`, `Id`, `Integer` or `String`. **Other types are not supported and will throw an exception**.
+`isIn` and `isNotIn` support a `Set` of one of the following types:
-Fields used in field criteria must be available on the list which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception can be thrown.
+- `Boolean`
+- `Date`
+- `Datetime`
+- `Decimal`
+- `Double`
+- `Id`
+- `Integer`
+- `Long`
+- `String`
-Fields that are present on the *prototype* object must also be available on the list which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception will be thrown.
+**Other types are not supported and will throw an exception**.
-Filtering query is dynamic and cannot be type-checked at compile-time.
+Fields used in field conditions must be available on the collection which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception can be thrown.
-### `GroupBy`
-
+#### `RecordMatch`
-Groups objects by values on a specified field.
+`RecordMatch` returns `true` if record fields are equal to those defined on a “prototype” record. Fields that are not
+defined on a prototype record do not have to match.
```java
-Map> opportunitiesByCloseDate = GroupBy.dates(Opportunity.CloseDate, opportunities);
+Account prototype = new Account(
+ Name = 'Test',
+ AnnualRevenue = 50000000
+);
+// Accounts named 'Test' with an AnnualRevenue of **exactly** 50,000,000 are matched
+Collection filtered = accountCollection.filter(Match.record(prototype));
```
-| Modifier and type | Method | Description |
-|-------------------|--------|-------------|
-| `Map>` | `booleans(Schema.SObjectField field, List records)` | Groups `records` by value on boolean `field` |
-| `Map>` | `booleans(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on boolean `field` as `listType` |
-| `Map>` | `dates(Schema.SObjectField field, List records)` | Groups `records` by value on date `field` |
-| `Map>` | `dates(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on date `field` as `listType` |
-| `Map>` | `decimals(Schema.SObjectField field, List records)` | Groups `records` by value on number `field` |
-| `Map>` | `decimals(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on number `field` as `listType` |
-| `Map>` | `ids(Schema.SObjectField field, List records)` | Groups `records` by value on id `field` |
-| `Map>` | `ids(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on id `field` as `listType` |
-| `Map>` | `strings(Schema.SObjectField field, List records)` | Groups `records` by value on string `field` |
-| `Map>` | `strings(Schema.SObjectField field, List records, Type listType)` | Groups `records` by value on string `field` as `listType` |
-
-#### Warning :warning:
+##### Warning :warning:
-**The type system will NOT warn you if you use the wrong subtype of `sObject`!** [Important notes on the type system in Apex](#type-system) section explains why.
-
-```java
-// this compiles
-Map> accountsByName = GroupBy.strings(Account.Name, accounts);
-// this compiles as well!!!???
-Map> accountsByName = GroupBy.strings(Account.Name, accounts);
-```
+Fields that are present on the *prototype* object must also be available on the collection which is filtered, otherwise a `System.SObjectException: SObject row was retrieved via SOQL without querying the requested field` exception will be thrown.
-### `Pick`
-
+### `remove`
+
-Picks fields from a list of sObjects to build a new list with just those fields. Helps reduce overwriting potential for concurrent updates when locking is not an option.
+`remove` works just like `filter`, but records which match a predicate are removed from the `Collection` view instead of kept.
-```java
-List opportunities = new List{
- new Opportunity(Name = 'Foo', Amount = 10000, Description = 'Bar')
-}
-// picked contains just Name and Amount fields, Description is not present
-List picked = Pick.fields(new Set{'Name', 'Amount'}, opportunities);
-```
-
-| Modifier and type | Method | Description |
-|-------------------|--------|-------------|
-| `List` | `fields(List fields, Iterable records)` | Picks fields into a new `SObject` list |
-| `List` | `fields(Set fields, Iterable records)` | Picks fields into a new `SObject` list |
-| `List` | `fields(List apiFieldNames, Iterable records)` | Picks fields into a new `SObject` list |
-| `List` | `fields(Set apiFieldNames, Iterable records)` | Picks fields into a new `SObject` list |
-
-### `Pluck`
+### `pluck`
-Plucks field values from a list of sObjects into a new list.
+Plucks field values from a `Collection` view of records into a `List` of appropriate type.
```java
List accounts = new List{
new Account(Name = 'Foo'),
new Account(Name = 'Bar')
}
-// Names are plucked into a new list ['Foo', 'Bar']
-List names = Pluck.strings(accounts, Account.Name);
+// Names are plucked into a new list, ['Foo', 'Bar']
+List names = Collection.of(accounts).pluckStrings(Account.Name);
```
-Pluck can also be used for relations with `String` parameters.
+Pluck can also be used for deeper relations by using `String` field paths instead of `Schema.SObjectField` parameters.
```java
List opportunities = new List{
@@ -174,24 +160,88 @@ List opportunities = new List{
};
// Names are plucked into a new list ['Foo', 'Bar']
-List accountNames = Pluck.strings(opportunities, 'Account.Name');
+List accountNames = Collection.of(opportunities).pluckStrings('Account.Name');
+```
+
+| Modifier and type | Method | Description |
+|-------------------|--------|-------------|
+| `List` | `pluckBooleans(Schema.SObjectField)` | Plucks Boolean `field` values |
+| `List` | `pluckBooleans(String relation)` | Plucks Boolean `relation` values |
+| `List` | `pluckDates(Schema.SObjectField field)` | Plucks Date `field` values |
+| `List` | `pluckDates(String relation)` | Plucks Date `relation` values |
+| `List` | `pluckDatetimes(Schema.SObjectField field)` | Plucks Datetime `field` values |
+| `List` | `pluckDatetimes(String relation)` | Plucks Datetime `relation` values |
+| `List` | `pluckDecimals(Schema.SObjectField field)` | Plucks numerical `field` values |
+| `List` | `pluckDecimals(Schema.SObjectField field)` | Plucks numerical `relation` values |
+| `List` | `pluckIds(Schema.SObjectField field)` | Plucks Id `field` values |
+| `List` | `pluckIds(String relation)` | Plucks Id `relation` values |
+| `List` | `pluckIds()` | Plucks values of `Id` field |
+| `List` | `strings(Schema.SObjectField field)` | Plucks String or Id `field` values |
+| `List` | `strings(Schema.SObjectField relation)` | Plucks String or Ids `relation` values |
+
+### `groupBy`
+
+
+Groups records by values of a specified field.
+
+```java
+Map> opportunitiesByCloseDate = Collection.of(opportunities).groupByDates(Opportunity.CloseDate, opportunities);
+```
+
+| Modifier and type | Method | Description |
+|-------------------|--------|-------------|
+| `Map>` | `groupByBooleans(Schema.SObjectField field)` | Groups records by Boolean `field` values |
+| `Map>` | `groupByBooleans(Schema.SObjectField field, Type listType)` | Groups records by Boolean `field` values, with specified list type |
+| `Map>` | `groupByDates(Schema.SObjectField field)` | Groups records by Date `field` values |
+| `Map>` | `groupByDates(Schema.SObjectField field, Type listType)` | Groups records by Date `field` values, with specified list type |
+| `Map>` | `groupByDatetimes(Schema.SObjectField field)` | Groups records by Datetime `field` values |
+| `Map>` | `groupByDatetimes(Schema.SObjectField field, Type listType)` | Groups records by Datetime `field` values, with specified list type |
+| `Map>` | `groupByDecimals(Schema.SObjectField field)` | Groups records by numeric `field` values |
+| `Map>` | `groupByDecimals(Schema.SObjectField field, Type listType)` | Groups records by numeric `field` values, with specified list type |
+| `Map>` | `groupByIds(Schema.SObjectField field)` | Groups records by Id `field` values |
+| `Map>` | `groupByIds(Schema.SObjectField field, Type listType)` | Groups records by Id `field` values, with specified list type |
+| `Map>` | `groupByStrings(Schema.SObjectField field)` | Groups records by String `field` values |
+| `Map>` | `groupByStrings(Schema.SObjectField field, Type listType)` | Groups records by String `field` values, with specified list type |
+
+### `pick`
+
+
+Returns a new `Collection` view of the collection which keeps just the specified fields, discarding others. Helps reduce overwriting potential for concurrent updates when locking is not an option.
+
+```java
+List opportunities = new List{
+ new Opportunity(Name = 'Foo', Amount = 10000, Description = 'Bar')
+}
+// Picked contains just Name and Amount fields. Description is not present.
+Collection picked = Collection.of(opportunities).pick(new Set{'Name', 'Amount'});
```
| Modifier and type | Method | Description |
|-------------------|--------|-------------|
-| `List` | `booleans(Schema.SObjectField field, List records)` | Plucks booleans of `field` into a new list |
-| `List` | `booleans(String relation)` | , List recordsPlucks booleans of `relation` into a new list |
-| `List` | `dates(Schema.SObjectField field, List records)` | Plucks dates of `field` into a new list |
-| `List` | `dates(String relation)` | , List recordsPlucks dates of `relation` into a new list |
-| `List` | `decimals(Schema.SObjectField field, List records)` | Plucks numbers of `field` into a new list |
-| `List` | `decimals(Schema.SObjectField field, List records)` | Plucks numbers of `relation` into a new list |
-| `Set` | `ids(Schema.SObjectField field, List records)` | Plucks ids of `field` into a new set |
-| `Set` | `ids(String relation)` | , List recordsPlucks ids of `relation` into a new set |
-| `Set` | `ids( | Plucks `, List recordsId` field values into a new set |
-| `List` | `strings(Schema.SObjectField field, List records)` | Plucks strings or ids of `field` into a new list |
-| `List` | `strings(Schema.SObjectField relation, List records)` | Plucks strings or ids of `relation` into a new list |
-
-### Important notes on the type system in Apex
+| `Collection` | `pick(List fields)` | Picks fields into a new `Collection` view |
+| `Collection` | `pick(Set fields)` | Picks fields into a new `Collection` view |
+| `Collection` | `pick(List apiFieldNames)` | Picks fields into a new `Collection` view |
+| `Collection` | `pick(Set apiFieldNames)` | Picks fields into a new `Collection` view |
+
+### `mapAll`
+
+
+Maps all elements of `Collection` view into another `Collection` view with the provided `SObjectToSObjectFunction` mapping function.
+
+| Modifier and type | Method | Description |
+|-------------------|--------|-------------|
+| `Collection` | `mapAll(SObjectToSObjectFunction fn)` | Returns a new `Collection` view formed by mapping all current view elements with `fn` |
+
+### `mapSome`
+
+
+Returns a new `Collection` view formed by mapping those view elements that satisfy `predicate`, and keeping those that do not unchanged.
+
+| Modifier and type | Method | Description |
+|-------------------|--------|-------------|
+| `Collection` | `mapAll(SObjectToSObjectFunction fn)` | Returns a new `Collection` view formed by mapping current view elements that satisfy `predicate` with `fn`, and keeping those that do not satisfy `predicate` unchanged. |
+
+## Important notes on the type system in Apex
Apex allows assignment of `SObject` collection to its “subclass”, and the other way around:
@@ -212,22 +262,13 @@ System.debug(objects instanceof List); // true
System.debug(objects instanceof List); // true
```
-Lambda classes usually return an `SObject` list, which can be then assigned to a specific `SObject` “subclass” list, like `Account`. This is more convenient, but `instanceof` can provide unexpected results:
+Collection’s `asList()` and `asSet()` return a raw `List` and `Set`. This is more convenient, but `instanceof` can provide unexpected results.
+A concrete type of the list can be passed in as well. When this is done, the returned `List` or `Set` are of the correct concrete type instead of generic `SObject` collection type:
```java
-List accounts = Filter...
-// accounts points to a List returned from Filter
+List filteredAccounts = accountCollection.asList();
+// List returned!
-Boolean isOpportunities = accounts instanceof List;
-// isOpportunities is true!!!???
-```
-
-`Filter` and `GroupBy` therefore provide overloaded methods in which the concrete type of the list can be passed in as well. When this is done, the returned `List` or `Map` are of the correct concrete type instead of generic `SObject` collection type:
-
-```java
-List filteredAccounts = Filter.field(...).apply(allAccounts, List.class);
+List filteredAccounts = accountCollection.asList(List.class);
// List returned!
-
-Map> accountsByName = GroupBy.strings(Account.Name, allAccounts, List.class);
-// Map> returned!
```
diff --git a/src/classes/Collection.cls b/src/classes/Collection.cls
new file mode 100644
index 0000000..4ae0f1d
--- /dev/null
+++ b/src/classes/Collection.cls
@@ -0,0 +1,318 @@
+public with sharing class Collection {
+
+ private List records;
+
+ public static Collection of(Iterable records) {
+ return new Collection(records);
+ }
+
+ public static Collection of(Set records) {
+ return new Collection(new List(records));
+ }
+
+ private Collection(Iterable records) {
+ this.records = new List();
+ Iterator iter = records.iterator();
+ while (iter.hasNext()) {
+ this.records.add(iter.next());
+ }
+ }
+
+ public List asList() {
+ return new List(records);
+ }
+
+ public List asList(Type listType) {
+ List typedList = (List) listType.newInstance();
+ typedList.addAll(records);
+ return typedList;
+ }
+
+ public Set asSet() {
+ return new Set(records);
+ }
+
+ public Set asSet(Type setType) {
+ Set typedSet = (Set) setType.newInstance();
+ typedSet.addAll(records);
+ return typedSet;
+ }
+
+ public Collection filter(SObjectPredicate predicate) {
+ List filtered = new List();
+ for (SObject record : records) {
+ if (predicate.apply(record)) {
+ filtered.add(record);
+ }
+ }
+ return Collection.of(filtered);
+ }
+
+ public Collection remove(SObjectPredicate predicate) {
+ List remaining = new List();
+ for (SObject record : records) {
+ if (!predicate.apply(record)) {
+ remaining.add(record);
+ }
+ }
+ return Collection.of(remaining);
+ }
+
+ public List pluckBooleans(Schema.SObjectField field) {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add((Boolean)rec.get(field));
+ }
+ return results;
+ }
+
+ public List pluckBooleans(String relation) {
+ List results = new List();
+ SObjectFieldReader reader = new SObjectFieldReader();
+ for (SObject rec : records) {
+ results.add((Boolean)reader.read(rec, relation));
+ }
+ return results;
+ }
+
+ public List pluckDates(Schema.SObjectField field) {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add((Date)rec.get(field));
+ }
+ return results;
+ }
+
+ public List pluckDates(String relation) {
+ List results = new List();
+ SObjectFieldReader reader = new SObjectFieldReader();
+ for (SObject rec : records) {
+ results.add((Date)reader.read(rec, relation));
+ }
+ return results;
+ }
+
+ public List pluckDatetimes(Schema.SObjectField field) {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add((Datetime)rec.get(field));
+ }
+ return results;
+ }
+
+ public List pluckDatetimes(String relation) {
+ List results = new List();
+ SObjectFieldReader reader = new SObjectFieldReader();
+ for (SObject rec : records) {
+ results.add((Datetime)reader.read(rec, relation));
+ }
+ return results;
+ }
+
+ public List pluckDecimals(Schema.SObjectField field) {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add((Decimal)rec.get(field));
+ }
+ return results;
+ }
+
+ public List pluckDecimals(String relation) {
+ List results = new List();
+ SObjectFieldReader reader = new SObjectFieldReader();
+ for (SObject rec : records) {
+ results.add((Decimal)reader.read(rec, relation));
+ }
+ return results;
+ }
+
+ public List pluckIds() {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add(rec.Id);
+ }
+ return results;
+ }
+
+ public List pluckIds(Schema.SObjectField field) {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add((Id)rec.get(field));
+ }
+ return results;
+ }
+
+ public List pluckIds(String relation) {
+ List results = new List();
+ SObjectFieldReader reader = new SObjectFieldReader();
+ for (SObject rec : records) {
+ results.add((Id)reader.read(rec, relation));
+ }
+ return results;
+ }
+
+ public List pluckStrings(Schema.SObjectField field) {
+ List results = new List();
+ for (SObject rec : records) {
+ results.add((String)rec.get(field));
+ }
+ return results;
+ }
+
+ public List pluckStrings(String relation) {
+ List results = new List();
+ SObjectFieldReader reader = new SObjectFieldReader();
+ for (SObject rec : records) {
+ results.add((String) reader.read(rec, relation));
+ }
+ return results;
+ }
+
+ public Map> groupByBooleans(Schema.SObjectField field, Type listType) {
+ Map> grouped = new Map>();
+ for (SObject rec : records) {
+ Boolean key = (Boolean) rec.get(field);
+ if (!grouped.containsKey(key)) {
+ grouped.put(key, (List) listType.newInstance());
+ }
+ grouped.get(key).add(rec);
+ }
+ return grouped;
+ }
+
+ public Map> groupByBooleans(Schema.SObjectField field) {
+ return groupByBooleans(field, List.class);
+ }
+
+ public Map> groupByDates(Schema.SObjectField field, Type listType) {
+ Map> grouped = new Map>();
+ for (SObject rec : records) {
+ Date key = (Date) rec.get(field);
+ if (!grouped.containsKey(key)) {
+ grouped.put(key, (List) listType.newInstance());
+ }
+ grouped.get(key).add(rec);
+ }
+ return grouped;
+ }
+
+ public Map> groupByDatetimes(Schema.SObjectField field) {
+ return groupByDatetimes(field, List.class);
+ }
+
+ public Map> groupByDatetimes(Schema.SObjectField field, Type listType) {
+ Map> grouped = new Map>();
+ for (SObject rec : records) {
+ Datetime key = (Datetime) rec.get(field);
+ if (!grouped.containsKey(key)) {
+ grouped.put(key, (List) listType.newInstance());
+ }
+ grouped.get(key).add(rec);
+ }
+ return grouped;
+ }
+
+ public Map> groupByDates(Schema.SObjectField field) {
+ return groupByDates(field, List.class);
+ }
+
+ public Map> groupByDecimals(Schema.SObjectField field, Type listType) {
+ Map> grouped = new Map>();
+ for (SObject rec : records) {
+ Decimal key = (Decimal) rec.get(field);
+ if (!grouped.containsKey(key)) {
+ grouped.put(key, (List) listType.newInstance());
+ }
+ grouped.get(key).add(rec);
+ }
+ return grouped;
+ }
+
+ public Map> groupByDecimals(Schema.SObjectField field) {
+ return groupByDecimals(field, List.class);
+ }
+
+ public Map> groupByIds(Schema.SObjectField field, Type listType) {
+ Map> grouped = new Map>();
+ for (SObject rec : records) {
+ Id key = (Id) rec.get(field);
+ if (!grouped.containsKey(key)) {
+ grouped.put(key, (List) listType.newInstance());
+ }
+ grouped.get(key).add(rec);
+ }
+ return grouped;
+ }
+
+ public Map> groupByIds(Schema.SObjectField field) {
+ return groupByIds(field, List.class);
+ }
+
+ public Map> groupByStrings(Schema.SObjectField field, Type listType) {
+ Map> grouped = new Map>();
+ for (SObject rec : records) {
+ String key = (String) rec.get(field);
+ if (!grouped.containsKey(key)) {
+ grouped.put(key, (List) listType.newInstance());
+ }
+ grouped.get(key).add(rec);
+ }
+ return grouped;
+ }
+
+ public Map> groupByStrings(Schema.SObjectField field) {
+ return groupByStrings(field, List.class);
+ }
+
+ public Collection pick(List fields) {
+ return pick(new Set(fields));
+ }
+
+ public Collection pick(Set fields) {
+ Set fieldNames = new Set();
+ for (Schema.SObjectField field : fields) {
+ Schema.DescribeFieldResult describe = field.getDescribe();
+ fieldNames.add(describe.getName());
+ }
+ return pick(fieldNames);
+ }
+
+ public Collection pick(Set apiFieldNames) {
+ List picked = new List();
+ for (SObject record : records) {
+ SObject result = record.getSObjectType().newSObject();
+ Map fieldMap = record.getPopulatedFieldsAsMap();
+ for (String fieldName : apiFieldNames) {
+ if (fieldMap.containsKey(fieldName)) {
+ result.put(fieldName, record.get(fieldName));
+ }
+ }
+ picked.add(result);
+ }
+ return Collection.of(picked);
+ }
+
+ public Collection pick(List apiFieldNames) {
+ return pick(new Set(apiFieldNames));
+ }
+
+ public Collection mapAll(SObjectToSObjectFunction fn) {
+ List mapped = new List