Last active
July 23, 2020 16:39
-
-
Save Viranchee/0c128b412e8ba7a42b08e77ecf0dbbf2 to your computer and use it in GitHub Desktop.
Solution to https://github.com/Fueled/exercise-swift-ios Interview Test by Fueled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
print("We started Again") | |
/*: | |
1. First, start by modeling the data objects that will be used. | |
*/ | |
/// TODO: Use `Tagged` library from PointFree.co | |
struct Comment: Codable { | |
var postId: Int | |
var email: String | |
var name: String | |
var body: String | |
var id: Int | |
} | |
struct Post: Codable { | |
var userId: Int | |
var id: Int | |
var title: String | |
var body: String | |
} | |
struct User: Codable { | |
var id: Int | |
var name: String | |
var username: String | |
var email: String | |
var address: Address | |
var phone: String | |
var website: String | |
var company: Company | |
struct Company: Codable { | |
var name: String | |
var catchPhrase: String | |
var bs: String | |
// Might be to expand `bs` property for being expressive and using a Coding Keys enum. | |
// But first get the solution working | |
} | |
struct Address: Codable { | |
var street: String | |
var suite: String | |
var city: String | |
var zipcode: String | |
var geo: Geo | |
struct Geo: Codable { | |
var lat: StringBacked<Double> | |
var lng: StringBacked<Double> | |
} | |
} | |
} | |
/// I created this type to take input Double wrapped in Strings, but then replaced the solution after finding John Sundell's implementation, given below | |
struct StringyFloat: Codable { | |
let rawValue: Double | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() // unkeyedContainer() | |
let str = try container.decode(String.self) | |
dump(str) | |
rawValue = Double(str) ?? 0 | |
} | |
} | |
/// John Sundell's code, https://www.swiftbysundell.com/articles/customizing-codable-types-in-swift/ | |
protocol StringRepresentable: CustomStringConvertible { | |
init?(_ string: String) | |
} | |
//extension Int: StringRepresentable {} | |
extension Double: StringRepresentable {} | |
struct StringBacked<Value: StringRepresentable>: Codable { | |
var value: Value | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
let string = try container.decode(String.self) | |
guard let value = Value(string) else { | |
throw DecodingError.dataCorruptedError( | |
in: container, | |
debugDescription: """ | |
Failed to convert an instance of \(Value.self) from "\(string)" | |
""" | |
) | |
} | |
self.value = value | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(value.description) | |
} | |
} | |
/*: | |
2. Next, decode the JSON source using `Resource.users.data()`. | |
*/ | |
let comments = try! JSONDecoder().decode([Comment].self, from: Resource.comments.data()) | |
let users = try! JSONDecoder().decode([User].self, from: Resource.users.data()) | |
let posts = try! JSONDecoder().decode([Post].self, from: Resource.posts.data()) | |
/*: | |
3. Next, use your populated models to calculate the average number of comments per user. | |
*/ | |
/// This time, let's interpret the logic a bit different. Let's do for `Avg No. of Comments Per Blog Post` | |
/// | |
/// Also, let's use the `Prelude Library`, my favourite! | |
/// https://github.com/pointfreeco/swift-prelude | |
/// | |
/// Let's use Xcode 12's feature, Swift Packages with Playgrounds | |
/// https://developer.apple.com/videos/play/wwdc2020/10096/ | |
/*: | |
Problem Breakdown: | |
## Let's modify the problem, I think it's not worded properly. | |
1. Get `number of comments` for each `post`, | |
Data Type: `Dictionary of [post id : number of comments]` | |
*/ | |
// Dictionaries are used as Caches instead of using pure functions. | |
/// [PostID: Number of Comments] | |
let commentsForPosts = comments.reduce(into: [:]) { (result, comment) in | |
result[comment.postId, default: 0] += 1 | |
} | |
/// [PostID : UserID] | |
let postIdtoUserID = posts.reduce(into: [:]) { (result, post) in | |
result[post.id] = post.userId | |
} | |
/// Find out the User who made a post, ideally from a comment | |
/// - Parameters: | |
/// - postID: the PostID to search for | |
/// - posts: All the posts | |
/// - Returns: UserID | |
func postIdtoUserID(postID: Int, posts: [Post] = posts) -> Int? { | |
return posts.first(where: \.id == postID)?.userId | |
} | |
/// [UserID : total comments received ] = [PostID : Number of Comments ], PostID -> UserID, add the comments which have the same UserID | |
/// - Note: Could be better modelled as a function taking `PostID -> UserID` conversion | |
let totalCommentsForUserID = commentsForPosts.reduce(into: Dictionary<Int,Int>()) { result, commentsForAPost in | |
let (postID, numberOfComments) = commentsForAPost | |
guard let userID = postIdtoUserID(postID: postID, posts: posts) else { return } | |
result[userID, default: 0] += numberOfComments | |
} | |
//[Blogger UserID : [(PostID : Engagement - Comments)] ] | |
let bloggerPostsAndComments: Dictionary<Int, [(postID:Int, engagement:Int)]> = | |
commentsForPosts.reduce(into: [:]) { (result, commentsInaPost) in | |
let (postID, numberOfComments) = commentsInaPost | |
guard let userID = postIdtoUserID(postID: postID, posts: posts) else { return } | |
result[userID, default: [] ] += [(postID:postID, engagement:numberOfComments)] | |
} | |
/*: | |
4. Finally, use your calculated metric to find the 3 most engaging bloggers, sort order, and output the result. | |
*/ | |
/*: | |
Problem Breakdown: | |
3 most engaging bloggers: | |
Bloggers post `Posts` and not `comments` | |
Engaging Blogger Metric: The one whose Blog post has most comments | |
So use the above metric of `number of comments / blog post` | |
Sort it | |
Take first 3 | |
*/ | |
print("- Formula1 One: Calculating user with Maximum Engagements") | |
totalCommentsForUserID | |
.lazy | |
.sorted { $0.value > $1.value } | |
.prefix(3) | |
// .forEach { dump($0) } | |
.map { (bloggerID, commentEngagements) -> (User, Int) in | |
return (users.first(where: \.id == bloggerID)!, commentEngagements) | |
} | |
.enumerated() | |
.forEach { (enumeratedBlogger) in | |
let (blogger, engagement) = enumeratedBlogger.element | |
let ranking = enumeratedBlogger.offset + 1 | |
print(" \(ranking). With \(engagement) engagements, comes \(blogger.name.capitalized) ") | |
} | |
print("- Formula1 Two: Calculating user with Most Average Engagements") | |
bloggerPostsAndComments | |
/// [1 blogger, (X blogs, Y comments for that blog)] -> [1 blogger, Z average comment per blog] | |
.mapValues{ (bloggerPostsAndComments) -> Double in | |
let totalEngagement = bloggerPostsAndComments.reduce(into: 0.0) { (result, postIdAndEngagement) in | |
result += Double(postIdAndEngagement.engagement) | |
} | |
return totalEngagement / Double(bloggerPostsAndComments.count) | |
} | |
.sorted {$0.value > $1.value } | |
.prefix(3) | |
///// [1 blogger, Z average comment per blog | |
.forEach { bloggerAndAvgEngagement in | |
let (bloggerID, engagement) = bloggerAndAvgEngagement | |
guard let name = users.first(where: \.id == bloggerID)?.name else { return } | |
print("\(name) - \(bloggerID), Score: \(engagement)") | |
} | |
//// .forEach {print($0)} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment