Skip to content

Instantly share code, notes, and snippets.

@Viranchee
Last active July 23, 2020 16:39
Show Gist options
  • Save Viranchee/0c128b412e8ba7a42b08e77ecf0dbbf2 to your computer and use it in GitHub Desktop.
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
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