CSCI 1200 - 作业 2 - 设计一个简单的 Uber
作业要求
Details
在本作业中,您将开发一个简单的拼车应用程序,名为纽约拼车。在开始编写作业代码之前,请仔细阅读整个说明。
学习目标
- 练习实现和使用 C++ 类。
- 练习使用
std::string
,std::vector
。
规范
纽约拼车应用程序应支持 2 种不同的角色:司机和乘客。乘客可以执行两个任务:
- 请求拼车
- 取消拼车请求
司机可以执行一个任务:
- 取消拼车请求
注意
像 Uber 或 Lyft 这样的商业拼车产品当然允许乘客和司机执行更多任务,但让我们诚实一点,Uber/Lyft 有数千名软件工程师,但你只有一人,只有一周时间来完成这个作业,所以让我们简化任务。
输入文件
像 Uber 和 Lyft 这样的公司会将所有司机和乘客的信息存储在他们的数据库中,但数据库远远超出了本课程的范围,因此我们将只在两个简单的文本文件中存储司机信息和乘客信息,即 drivers.txt 和 riders.txt。在本作业中,您将再次读取这些文件作为程序的输入,解析它们以检索司机和/或乘客的信息,并将它们存储在您自己的数据结构中。在本作业中,您必须使用 std::vector
存储司机和乘客。建议使用一个 std::vector
实例存储所有司机,使用另一个 std::vector
实例存储所有乘客。
司机信息
drivers.txt 文件的格式如下:
|
|
以上是 drivers.txt 文件的前 10 行。它有 13 个字段,由空格分隔。这 13 个字段是:
- 司机的名
- 司机的姓
- 司机的性别
- 司机的年龄
- 司机的电话号码
- 司机的评分
- 司机的当前纬度
- 司机的当前经度
- 司机的车辆类型
- 司机的当前状态
- 乘客的名
- 乘客的姓
- 乘客的电话号码
最后三个字段只有在拼车请求分配给该司机时才有意义。在本作业中,我们假设当请求分配给该司机时,司机将接受该请求。
司机可以处于以下状态之一:
- 可用(等待请求)
- 正在前往接客地点(请求已接受)
- 在行程中
当司机处于可用状态时,表示该司机未被分配拼车请求,因此不与任何乘客关联,因此该司机的最后三个字段将只是
|
|
乘客信息
riders.txt 文件的格式如下:
|
|
以上是 riders.txt 文件的前 10 行。它有 17 个字段,由空格分隔。这 17 个字段是:
- 乘客的名
- 乘客的姓
- 乘客的性别
- 乘客的年龄
- 乘客的电话号码
- 乘客的评分
- 乘客的接客地点名称
- 乘客的接客地点纬度
- 乘客的接客地点经度
- 乘客的下车地点名称
- 乘客的下车地点纬度
- 乘客的下车地点经度
- 乘客的车辆偏好
- 乘客的当前状态
- 司机的名
- 司机的姓
- 司机的电话号码
乘客可以处于以下状态之一:
- 准备请求
- 司机正在前往接客地点
- 在行程中
理想情况下,应该有四个状态,另一个状态是:拼车请求已发出但尚未被任何司机接受。然而,如我们所述,在本作业中,我们假设当乘客发出请求时,它将被司机接受,因此我们可以从考虑中排除此状态。
当乘客处于 Ready_to_request
状态时,表示没有司机现在被分配给这个拼车请求,因此该乘客的最后三个字段将只是
|
|
支持的命令
您的程序只需要支持两个命令:
拼车请求
第一个命令允许乘客发送拼车请求。
|
|
其中
drivers.txt
是包含所有司机信息的输入文件。您的程序不应更改此文件。riders.txt
是包含所有乘客信息的输入文件。您的程序不应更改此文件。output0.txt
是输出文件,您需要在此打印消息给乘客或司机。output1.txt
是输出文件,您需要在此打印更新后的司机信息,因此此文件应具有与drivers.txt
相同的格式。output2.txt
是输出文件,您需要在此打印更新后的乘客信息,因此此文件应具有与riders.txt
相同的格式。phoneNumber
。理想情况下,这应该是一个与riders.txt
中某个乘客的Ready_to_request
状态相对应的电话号码;但生活并不总是理想,如何处理各种电话号码情况将在本节中描述。request
表示这是一个拼车请求。
当运行此命令时,如果
-
找到司机,您的程序应
-
将以下信息打印到
output0.txt
文件中:1 2 3 4
为乘客 Rebecca 请求拼车,寻找经济型车辆。 接客地点:Williamsburg,下车地点:Statue_of_Liberty。 我们已为您找到最近的司机 Elena(4.7)。 Elena 现在距离您 7.9 英里。
将
Rebecca
替换为乘客的名,将经济型
替换为乘客的偏好车辆类型,将Williamsburg
替换为乘客的接客地点,将Statue_of_Liberty
替换为乘客的下车地点。将Elena
曹换为司机的名,将4.7
替换为司机的评分。将7.9
替换为司机与乘客之间的距离。 -
将更新后的
drivers.txt
打印到output1.txt
。 -
将更新后的
riders.txt
打印到output2.txt
。
-
-
如果找不到司机,您的程序应将以下消息打印到
output0.txt
文件中:1 2 3
为乘客 Isabella 请求拼车,寻找豪华型车辆。 接客地点:Williamsburg,下车地点:Boerum_Hill。 抱歉,目前我们无法为您找到司机。
将
Isabella
替换为乘客的名,将豪华型
替换为乘客的偏好车辆类型,将Williamsburg
替换为乘客的接客地点,将Boerum_Hill
替换为乘客的下车地点。 -
如果从命令行提供的电话号码格式不是 xxx-xxx-xxxx,您的程序应将以下消息打印到
output0.txt
文件中:1
电话号码无效。
-
如果从命令行提供的电话号码与任何乘客的电话号码都不匹配,您的程序应将以下消息打印到
output0.txt
文件中:1
账户不存在。
-
如果发出请求的乘客处于
Driver_on_the_way
状态,您的程序应将以下消息打印到output0.txt
文件中:1
您已经请求了拼车,您的司机正在前往接客地点。
-
如果发出请求的乘客处于
During_the_trip
状态,您的程序应将以下消息打印到output0.txt
文件中:1
您目前无法请求拼车,因为您已经在行程中。
取消请求
第二个命令允许乘客或司机取消请求。请注意,乘客和司机都有权取消请求。
|
|
此命令与第一个命令的区别在于此处的最后一个参数是 cancel,而在第一个命令中,最后一个参数是 request
。
当乘客取消请求时,您只需取消请求;当司机取消请求时,您应取消请求,同时为该乘客寻找另一个最近的司机。
只有那些正在前往接客地点的司机或司机正在前往的乘客才允许取消请求。
当运行此第二个命令时,如果
-
从命令行提供的电话号码与任何乘客的电话号码都不匹配,也不与任何司机的电话号码匹配,您的程序应将以下消息打印到
output0.txt
文件中:1
账户不存在。
-
如果取消请求是由乘客发出的,而该乘客的状态不是
Driver_on_the_way
,您的程序应将以下消息打印到output0.txt
文件中:1
只有当您的司机正在前往接客地点时,您才能取消拼车请求。
-
如果取消请求是由司机发出的,而该司机的状态不是
On_the_way_to_pickup
,您的程序应将以下消息打印到output0.txt
文件中:1
只有当您正在前往接客地点时,您才能取消拼车请求。
-
如果取消请求是由乘客发出的,而该乘客的状态是
Driver_on_the_way
,您的程序应:-
将以下消息打印到
output0.txt
文件中:1
乘客 Brenda 的拼车请求已被乘客取消。
-
将更新后的
drivers.txt
打印到output1.txt
:司机的状态应从On_the_way_to_pickup
改为Available
,并且司机的最后三个字段应重置为null
,表示该司机现在不与任何乘客关联。 -
将更新后的
riders.txt
打印到output2.txt
:乘客的状态应从Driver_on_the_way
改为Ready_to_request
,并且乘客的最后三个字段应重置为null
,表示现在没有司机与该乘客关联。
-
-
如果取消请求是由司机发出的,而该司机的状态是
On_the_way_to_pickup
,您的程序应:-
将以下消息打印到
output0.txt
文件中:1 2 3 4 5
您的司机 Edward 取消了拼车请求。我们现在将为您寻找新司机。 乘客 Angela 请求拼车,寻找标准型车辆。 接客地点:The_Met_Cloisters,下车地点:Brooklyn_Navy_Yard。 我们已为您找到最近的司机 Robert(3.2)。 Robert 现在距离您 2.1 英里。
将
Edward
替换为司机的名。将Angela
替换为乘客的名,将标准型
替换为乘客的偏好车辆类型。将The_Met_Cloisters
替换为乘客的接客地点,将Brooklyn_Navy_Yard
替换为乘客的下车地点。将Robert
替换为新司机的名,将3.2
替换为新司机的评分。将2.1
替换为新司机与乘客之间的距离。 -
将更新后的
drivers.txt
打印到output1.txt
:旧司机的状态应从On_the_way_to_pickup
改为Available
。应分配新司机,并相应更新其状态。同时,旧司机应不再与该乘客关联,新司机现在应与该乘客关联。 -
将更新后的
riders.txt
打印到output2.txt
:乘客现在应与新司机关联。
-
基于哈弗辛公式计算距离
在寻找司机时,您必须始终找到与乘客偏好车辆类型匹配的最近司机。并且当找到最近的司机时,您还需要打印该司机与乘客之间的距离。因此,您需要一种计算两个坐标之间距离的方法,为此目的,在本作业中,您将使用哈弗辛公式,下面是使用哈弗辛公式的代码:
|
|
此函数接受四个参数,即两个地理位置的纬度和经度,并返回这两个位置之间的距离(单位为英里)。此函数调用了几个数学库函数,因此您需要包含 cmath
库:
|
|
包含保护
如果您正在编写多个类,可能会在编译时遇到奇怪的编译器错误。这可能是由于包含类文件的问题,可以通过以下方式解决:对于名为 myclass.h
的头文件,在头文件的最顶部添加以下两行:
|
|
并在您的 .h
文件的最底部添加这一行:
|
|
这种技术称为“包含保护”。包含保护确保无论包含多少次,编译器只会处理头文件一次。
常见问题
问:乘客请求的车辆类型是寻找匹配司机的严格要求,还是只是偏好?换句话说,如果乘客请求经济型,但没有可用的经济型司机,但有其他车辆类型的可用司机,我们应该输出找不到司机,还是匹配最近的其他车辆类型司机?
答:这是严格要求。不要为乘客选择其他车辆类型。
问:输出距离的精度是多少?是小数点后一位,还是有效数字,还是保留一定数量的空格?我们是向上取整、向下取整还是直接截断?
答:与 Uber 相同。小数点后一位。直接截断即可。例如,如果距离是 11.4571 英里,您应该输出 11.4 英里,而不是 11.5 英里。
程序要求与提交详情
在本作业中,您需要使用 vector 存储所有司机,并使用 vector 存储所有乘客。您不允许使用我们尚未学习的任何数据结构,特别是 std::list
。您的程序应包含至少两个具有自己 .h
和 .cpp
文件的类,命名应适当。
在设计和实现程序时使用良好的编码风格。将程序组织成函数:
不要将所有代码放在 main 函数中!在完成解决方案时,请务必阅读 Homework Policies。务必创建新的测试用例来彻底调试您的程序,并不要忘记注释您的代码!使用提供的模板 README.txt 文件来写注释,供评分者阅读。
您必须按照 Collaboration Policy & Academic Integrity 页面所述独立完成此作业。如果您与他人讨论了问题或错误信息等,请在您的 README.txt
文件中列出他们的名字。
截止日期:2025 年 01 月 23 日,星期四,晚上 10 点。
评分标准
14 分
- 完成 README.txt (3 分)
- 未填写姓名、合作者或工作时间中的一个。(-1)
- 未填写姓名、合作者或工作时间中的两个或更多。(-2)
- 没有反思。(-1)
- 整体类声明与实现及编码风格 (良好的类设计,分成 .h 和 .cpp 文件。函数超过一行的放在 .cpp 文件中。类实现组织良好,注释合理。正确使用 const/const& 和类方法 const。) (6 分)
- 没有分数(实现严重不完整)。(-6)
- 无法成功声明和使用任何新类。(-6)
- 仅声明/使用一个类。(-5)
- 几乎所有代码都放在 main 函数中。最好为不同任务创建单独的函数。(-2)
- 错误使用或遗漏 const 和引用。(-1)
- 函数体中包含超过一条语句的放在 .h 文件中。(对于模板类是允许的)(-2)
- 函数未充分文档化或注释不充分,无论是在 .h 还是 .cpp 文件中。(-1)
- 代码排版拥挤,空格过多或缩进不当。(-1)
- 文件组织不良:将多个类放在一个文件中(对于非常小的辅助类是允许的)(-1)
- 变量命名选择不当:非描述性名称(例如 ‘vec’,‘str’,‘var’),单字母变量名称(除了单个循环计数器)等。(-2)
- 使用全局变量。(-1)
- 数据表示(必须使用 vectors 实现)(5 分)
- 没有分数(实现严重不完整)。(-5)
- 未使用 std::vector 存储司机或乘客。(-5)
- 使用 std::list 或本课程未涉及的数据结构。(-5)
- 成员变量是公共的。(-2)
支持文件
ride_sharing.7z程序设计
由于这次作业比较复杂,我们在开始前最好好好构思一下怎么实现。这里我借用两种情况,也就是指令是“request”和“cancel”两种情况分别设计它们单独的流程,然后在主程序中使用这两部分。姑且把它们称作handleRequest()
以及handleCancel()
。
由于流程图比较大,我暂时把它转换成了图片。
Mermaid 源码如下:
|
|
有了流程图我们就可以慢慢实现它们了。当然,不一定要真写handleRequest()
和handleCancel()
两个function。把它们的逻辑直接在main()
中实现也是可以的,我这里只是为了思路清晰而已。
此外,我们要设计两个Class,因为这是作业要求,也是为了方便我们实现功能。
classDiagram class Rider { %% 数据成员 - firstName : - lastName : string - gender : string - age : int - phoneNumber : string - rating : double - pickupLocationName : string - pickupLatitude : double - pickupLongitude : double - dropoffLocationName : string - dropoffLatitude : double - dropoffLongitude : double - vehiclePref : string - currentState : string - driverFirstName : string - driverLastName : string - driverPhoneNumber : string %% 构造函数 + Rider() %% 方法 + getFirstName() + getLastName() + getGender() + getAge() + getPhoneNumber() + getRating() + getPickupLocationName() + getPickupLatitude() + getPickupLongitude() + getDropoffLocationName() + getDropoffLatitude() + getDropoffLongitude() + getVehiclePref() + getCurrentState() + getDriverFirstName() + getDriverLastName() + getDriverPhoneNumber() + setCurrentState() + setDriverInfo() + toFileString() } class Driver { %% 数据成员 - firstName : string - lastName : string - gender : string - age : int - phoneNumber : string - rating : double - latitude : double - longitude : double - vehicleType : string - currentState : string - riderFirstName : string - riderLastName : string - riderPhoneNumber : string %% 构造函数 + Driver() %% 方法 + getFirstName() + getLastName() + getGender() + getAge() + getPhoneNumber() + getRating() + getLatitude() + getLongitude() + getVehicleType() + getCurrentState() + getRiderFirstName() + getRiderLastName() + getRiderPhoneNumber() + setCurrentState() + setRiderInfo() + toFileString() }classDiagram class Rider { %% 数据成员 - firstName : - lastName : string - gender : string - age : int - phoneNumber : string - rating : double - pickupLocationName : string - pickupLatitude : double - pickupLongitude : double - dropoffLocationName : string - dropoffLatitude : double - dropoffLongitude : double - vehiclePref : string - currentState : string - driverFirstName : string - driverLastName : string - driverPhoneNumber : string %% 构造函数 + Rider() %% 方法 + getFirstName() + getLastName() + getGender() + getAge() + getPhoneNumber() + getRating() + getPickupLocationName() + getPickupLatitude() + getPickupLongitude() + getDropoffLocationName() + getDropoffLatitude() + getDropoffLongitude() + getVehiclePref() + getCurrentState() + getDriverFirstName() + getDriverLastName() + getDriverPhoneNumber() + setCurrentState() + setDriverInfo() + toFileString() } class Driver { %% 数据成员 - firstName : string - lastName : string - gender : string - age : int - phoneNumber : string - rating : double - latitude : double - longitude : double - vehicleType : string - currentState : string - riderFirstName : string - riderLastName : string - riderPhoneNumber : string %% 构造函数 + Driver() %% 方法 + getFirstName() + getLastName() + getGender() + getAge() + getPhoneNumber() + getRating() + getLatitude() + getLongitude() + getVehicleType() + getCurrentState() + getRiderFirstName() + getRiderLastName() + getRiderPhoneNumber() + setCurrentState() + setRiderInfo() + toFileString() }
踩坑内容
-
在打印
output0.txt
时,looking for 后面要跟一个 Rider 期望的vehicle。但是这个vehicle可能有元音字母,所以要判断一下,到底是用a还是an。这是一个小问题,实现起来并不复杂,但是别忘了。我用了一个function来返回a还是an。1 2 3 4 5 6 7
std::string autoAAn(const std::string &word) { if (word.empty()) return ""; if (word[0] == 'A' || word[0] == 'E' || word[0] == 'I' || word[0] == 'O' || word[0] == 'U') { return "an"; } return "a"; }
然后在后面配合
<sstream>
的ostringstream
的方式拼接从Rider Class获取的字段。下面的代码就展示以下逻辑和结构,不必较真。1 2 3 4 5 6 7 8 9
#include <sstream> //... Rider &r = riders[rIdx]; //just consider `r` as your rider class std::ostringstream msg; msg << "Ride requested for rider " << r.getFirstName() << ", looking for " << autoAAn(r.getVehiclePref()) << " " << r.getVehiclePref() << " vehicle.\n" << "Pick Up Location: " << r.getPickupLocationName() << ", Drop Off Location: " << r.getDropoffLocationName() << ".\n";
-
在处理司机状态的时候要特别小心,一旦有变化要马上更新,因为一些逻辑是依赖于司机状态的。比如当司机从
On_the_way_to_pickup
变为Available
时,需要清空Rider关联信息,并且再自动为Rider找新司机。重新寻找司机的时候,也要避免重新找到之前已经分配过的司机。 -
Class的变量不能是Public(作业要求)的,调用内容必须用方法,而不是直接对变量操作。
-
根据作业要求,我们不能用
auto
类型,这导致我不得不修改了for循环的内容,比如:1 2 3 4 5 6 7 8 9 10
void exportDrivers(const std::string &filename, const std::vector<Driver> &drivers) { //... std::ofstream ofs(filename); - for (const auto &d : drivers) { + for (int i = 0; i < (int)drivers.size(); i++) { + const Driver &d = drivers[i]; ofs << d.toFileString() << "\n"; } ofs.close(); }
参考代码
nyrider.cpp
|
|
Rider.h
|
|
Rider.cpp
|
|
Driver.h
|
|
Driver.cpp
|
|
相关内容
- CSCI 1100 - 作业 1 - 计算与字符串处理
- CSCI 1100 - 作业 8 - 熊、浆果与游客重聚:类
- CSCI 1100 - 作业 7 - 字典
- CSCI 1100 - 作业 6 - 文件、集合和文档分析
- CSCI 1100 - 作业5 - 嵌套列表、网格和路径规划