Skip to main content

Fluent Filter and JSON

  • Please download the homework here

Overview

During this project, you will implement a fluent filter for a list of businesses. You will be given a large set of business data and you are tasked with implementing a class that will filter the data on various conditions.

Learning Objectives

  • Practice writing class-based TypeScript
  • Learn about JavaScript Object Notation (JSON)

Student Expectations

Students will be graded on their ability to:

Testing

You must write tests for all your functions, following the principles used so far.

Getting Started

JSON

JavaScript Object Notation (JSON) is a syntax for representing JavaScript objects, arrays, numbers, strings, booleans, and null as a complete (single) string. With this format, we can store runtime data on the disk or send it over a network.

Below is a JavaScript object,

const sandwichOrders = {
orders: [
{
orderId: 1234,
sandwiches: [
{
bread: "whole wheat",
vegetables: ["lettuce", "onion", "tomato", "hot peppers"],
meat: ["prosciutto", "salami"],
cheese: ["provolone"],
condiments: ["mayonnaise", "vinegar"],
isToasted: false,
},
{
bread: "white",
vegetables: null,
meat: ["bologna"],
cheese: null,
condiments: null,
isToasted: false,
},
],
total: 13.4,
},
],
};

and its JSON representation:

const str = JSON.stringify(sandwichOrders);
console.log(str);
// {"orders":[{"orderId":1234,"sandwiches":[{"bread":"whole wheat","vegetables":["lettuce","onion","tomato","hot peppers"],"meat":["prosciutto","salami"],"cheese":["provolone"],"condiments":["mayonnaise","vinegar"],"isToasted":false},{"bread":"white","vegetables":null,"meat":["bologna"],"cheese":null,"condiments":null,"isToasted":false}],"total":13.4}]}

We used the JSON.stringify function to convert our object into a string. All fields and values were placed into this string in a format that is readable:

{
"orders": [
{
"orderId": 1234,
"sandwiches": [
{
"bread": "whole wheat",
"vegetables": ["lettuce", "onion", "tomato", "hot peppers"],
"meat": ["prosciutto", "salami"],
"cheese": ["provolone"],
"condiments": ["mayonnaise", "vinegar"],
"isToasted": false
},
{
"bread": "white",
"vegetables": null,
"meat": ["bologna"],
"cheese": null,
"condiments": null,
"isToasted": false
}
],
"total": 13.4
}
]
}

Notice how all fields have been surrounded by double quotes. The quotes can be dropped in JS/TS - but JSON format indicates that fields are surrounded with double quotes. We can then transform this string back into an object using the JSON.parse function:

// JSON.parse(s: string): any
const other = JSON.parse(str);
console.log(other); // { orders: [Object] }
// `other` is an entirely different object than `obj`

The Yelp Dataset

The business review site Yelp releases a large dataset of businesses in a JSON format. In this assignment, you will use this dataset to answer vital questions such as “What is the most popular business in California?"

Each entry in the dataset JSON object, and looks something like this:

[
{
"business_id": "Pns2l4eNsfO8kk83dixA6A",
"name": "Abby Rappoport, LAC, CMQ",
"address": "1616 Chapala St, Ste 2",
"city": "Santa Barbara",
"state": "CA",
"postal_code": "93101",
"latitude": 34.4266787,
"longitude": -119.7111968,
"stars": 5.0,
"review_count": 7,
"is_open": 0,
"categories": [
"Doctors",
"Traditional Chinese Medicine",
"Naturopathic/Holistic",
"Acupuncture",
"Health & Medical",
"Nutritionists"
]
},
{
"business_id": "mpf3x-BjTdTEA3yCZrAYPw",
"name": "The UPS Store",
"address": "87 Grasso Plaza Shopping Center",
"city": "Affton",
"state": "MO",
"postal_code": "63123",
"latitude": 38.551126,
"longitude": -90.335695,
"stars": 3.0,
"review_count": 15,
"is_open": 1,
"attributes": { "BusinessAcceptsCreditCards": true },
"categories": ["Shipping Centers", "Local Services", "Notaries", "Mailbox Centers", "Printing Services"],
"hours": {
"Monday": "0:0-0:0",
"Tuesday": "8:0-18:30",
"Wednesday": "8:0-18:30",
"Thursday": "8:0-18:30",
"Friday": "8:0-18:30",
"Saturday": "8:0-14:0"
}
}
// ...thousands of other entries
]

Notice how unorganized and "dirty" the data is. As examples,

  • the attributes field is absent in the first object, but present with data in the second
  • the hours field is absent in the first object, but present with data in the second

With thousands of other data entries, it is not hard to imagine there are dozens of other abnormalities in the data.

Provided to you is a version of the Yelp dataset, you may load it memory by using the loadYelpData(part?: number) function (see the examples in ./src/main.ts). There are other functions implemented inside of ./include/data.ts that could be useful. Such as creating randomized Businesses with createRandomData(n: number), or creating a dataset with a name, but reusing results from previous runs (loadOrCreate(datasetName: string, createDataset: () => Business[])).

Examine the results of these two functions - look at the fields on each entry and the types they typically hold. Additionally, inside of the ./include/data/ folder, there is a series of JSON files that you can use to inspect the dataset.

Type Safety with JSON

The return type of JSON.parse is any. The any type is a special type that allows any operation to occur on it - without a compiler error.

const obj = JSON.parse("{}");

// No error
const y = obj.x + 1;

As you can see, behavior is dangerous. obj.x clearly is not a value and we are attempting to do arithmetic with it. If you run this code you will get NaN, which is a special number value short for Not-A-Number. Yet the compiler does not warn us that obj.x is possibly undefined.

To aid the compiler and enable type safety, we need to construct a type that dictates what the result of JSON.parse may be. In this project, you will interacting with Business objects, typed as so:

interface BusinessAttributes {
// We do not know all the mappings for Ambience, nor do we need to know, so they remain unknown
Ambience?: Record<string, unknown>;
// Attributes has fields other than Ambience, but we do not need to type them specifically
[key: string]: unknown;
}

export interface Business {
business_id: string;
name?: string;
city?: string;
state?: string;
postal_code?: string;
latitude?: number;
longitude?: number;
stars?: number;
review_count?: number;
attributes?: BusinessAttributes;
categories?: string[];
hours?: Record<string, string>;
}

Notice how each field ends with a ?. This means that the field may or may not be present inside the object. It is similar to writing the field as name: string | undefined. However, make note that we are assuming that as long as the fields are present the types are as expected.

The Record<T, U> type, see documentation here, is an object type that has keys of type T, which map to values of type U. A Record is just an object, so we can do normal object operations on it, such as checking if a string value exists inside the record:

const b: Business = {
/* ... */
};
const hasMondaySchedule = b.hours !== undefined && "Monday" in b.hours;

const dayOfTheWeek = "Wednesday";
const hasWednesdaySchedule = b.hours !== undefined && dayOfTheWeek in b.hours;

To ensure that a field is safe to work with, we should check to make sure it is not undefined:

if (b.stars !== undefined) {
// OK
}

You may find yourself in a scenario where the compiler is unable to correctly type a value:

const key = "stars";

if (b[key] === undefined) return;

// s: number | undefined
const s = b[key];

Clearly, the value of s is not undefined. There are ways of cleanly resolving this using more advanced TypeScript features, but for our purposes, you can use a non-null assertion.

TypeScript also has a special syntax for removing null and undefined from a type without doing any explicit checking. Writing ! after any expression is effectively a type assertion that the value isn’t null or undefined

// s: number
const s = b[key]!;

This will cause a linter warning - however linting is disabled for this project.

Avoiding Code Duplication

You may notice that the first first five methods do a similar operation. They are filtering by if a value at a key is present and meets a specific condition. To avoid duplication, you may need to write a general function to filter out objects that are missing a field, otherwise filtering by some condition. The type keyof Business is a union of all the keys of Business.

function hasProperty(business: Business, key: keyof Business) {
return business[key] !== undefined;
}

hasProperty(b, "name"); // OK
hasProperty(b, "stars"); // OK
hasProperty(b, "owner"); // Complier error

To handle the latter case (the specific filtering condition), you may need to use the any type. There are clean ways around this, but they are more complicated and distract from the goal of the project. This will cause a linter warning; however, linting is ignored for this project.

Other methods may require working with specific keys. We can create a type that unions those keys, and specify the parameters to only accept that type.

type SortKey = "review_count" | "stars";

function sortBy(businesses: Business[], keys: SortKey) {
// Sort `businesses` by the key specified by `key`.
}

sortBy([], "stars"); // OK
sortBy([], "name"); // Compiler error

Programming Tasks

Create and implement the following methods in the FluentBusinesses class (in ./src/FluentBusinesses.ts).

You may not:

  • Use any for, while, do-while, or .forEach(...) loops
  • Use duplicate code across numerous methods (you will need to define helper methods)
  • Assume anything about the structure of an object in the data set (fields may, or may not, be present)

fromCityInState

Implement the following method:

fromCityInState(city: string, state: string): FluentBusinesses {
// TODO
}

It takes two strings, city and state, and returns a new FluentBusinesses object in which all businesses are located in the given city and state.

Use the city and state fields.

hasStarsGeq

Implement the following method:

hasStarsGeq(stars: number): FluentBusinesses {
// TODO
}

It takes a number, stars, and returns a new FluentBusinesses object that holds businesses with star ratings that are greater than or equal to stars.

Use the stars field.

inCategory

Implement the following method:

inCategory(category: string): FluentBusinesses {
// TODO
}

It takes a string, category, and produces a new FluentBusinesses object that holds only those businesses that have the provided category, category.

Use the categories field.

hasHoursOnDays

Implement the following method:

hasHoursOnDays(days: string[]): FluentBusinesses {
// TODO
}

It takes in an array of strings, days, and produces a new FluentBusinesses object that holds only those businesses that have hours during all of those days. You do not need to check if those hours are anything valid.

Use the hours field.

hasAmbience

Implement the following method:

hasAmbience(ambience: string): FluentBusinesses

It takes a string, ambience, and produces a new FluentBusinesses object with businesses that have the provided ambience, ambience. A given business has an ambience if the field is present and the value is true.

Use the Ambience field of object in the attributes field.

bestPlace

Implement the following method:

bestPlace(): Business | undefined {
// TODO
}

It returns the “best” business. The “best” business has the highest star rating. If there is a tie, pick the one with the most reviews. If there’s a tie with the most reviews, pick the first business. If there is no matching result, it should return undefined.

Use the stars and review_count fields.

mostReviews

Implement the following method:

mostReviews(): Business | undefined {
// TODO
}

It returns the "most reviewed" business. If there is a tie, pick the one with the most stars. If there is still a tie, pick the first business. If there is no matching result, it should return undefined.

Use the stars and review_count fields.