CSCI 1200 - 作业 2 - 设计一个简单的 Uber
作业要求
Details
在这个作业中,你将开发一个名为 New York Ride 的简单拼车应用程序。请在开始编写代码前阅读整个说明文件。
学习目标
- 练习实现和使用 C++ 类。
- 练习使用字符串类(std::string)和向量容器(std::vector)。
规范
New York Ride 应用程序应该支持两种不同的角色:司机、乘客。乘客可以执行以下任务:
- 请求乘车
- 取消乘车请求
司机可以执行一个任务:
- 取消乘车请求
注意: 一个像 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 个字段分别是:
- 乘客的名字
- 乘客的姓氏
- 乘客的性别
- 乘客的年龄
- 乘客的电话号码
- 乘客的评分
- 接客地点名称(如果分配了乘车请求)
- 接客地点的位置纬度(如果分配了乘车请求)
- 接客地点的位置经度(如果分配了乘车请求)
- 目的地名称(如果分配了乘车请求)
- 目的地位置的纬度(如果分配了乘车请求)
- 目的地位置的经度(如果分配了乘车请求)
- 乘客偏好的车辆类型
- 乘客当前的状态
- 司机的名字(如果分配了乘车请求)
- 司机的姓氏(如果分配了乘车请求)
- 司机的电话号码(如果分配了乘车请求)
一个乘客可以处于以下状态之一:
- 准备请求乘车
- 司机正在前往接客地点(已接受请求)
- 在行程中
理想情况下,应该有四个状态,另一个状态是:已经发出请求但尚未被任何司机接受。然而,正如我们提到的,在这个作业中,假设当乘客发出请求时,它会被一个司机接受,因此我们可以排除这种状态。
当乘客处于准备请求乘车的状态时,这意味着没有司机现在被分配到该乘车请求,并且因此,该乘客的最后三个字段将只是
|
|
支持的命令
你的程序只需要支持两个命令:
乘车请求
第一个命令允许乘客发送一个乘车请求。
|
|
这里
drivers.txt
是包含所有司机信息的输入文件。你的程序不应该改变这个文件。riders.txt
是包含所有乘客信息的输入文件。你的程序不应该改变这个文件。output0.txt
是你向乘客或司机打印消息的输出文件。output1.txt
是更新后的司机信息,因此该文件应该与 drivers.txt 格式相同。output2.txt
是更新后的乘客信息,因此该文件应该与 riders.txt 格式相同。phoneNumber
。理想情况下这应该是与 riders.txt 中状态为 “Ready_to_request” 的一个乘客的电话号码对应的;但生活不总是理想的,并且你的程序如何处理各种电话号码情况将在本节中描述。request
表示这是一个乘车请求。
当运行此命令时,如果找到司机,则
- 你应该在 output0.txt 文件中打印以下信息:
|
|
用乘客的名字替换 Rebecca,用乘客偏好的车辆类型替换 Economy,用接客地点名称替换 Williamsburg,用目的地名称替换 Statue_of_Liberty。用司机的名字替换 Elena,用司机的评分替换 4.7。用司机距离乘客的距离替换 7.9。
1.2 在 output1.txt 文件中打印更新后的 drivers.txt 版本。 1.3 在 output2.txt 文件中打印更新后的 riders.txt 版本。
- 如果找不到司机,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
用乘客的名字替换 Isabella,用乘客偏好的车辆类型替换 Luxury,用接客地点名称替换 Williamsburg,用目的地名称替换 Boerum_Hill。
- 如果从命令行提供的电话号码不符合 xxx-xxx-xxxx 格式,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
- 如果从命令行提供的电话号码与任何乘客的电话号码都不匹配,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
- 如果发出此请求的乘客处于 “Driver_on_the_way” 状态,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
- 如果发出此请求的乘客处于 “During_the_trip” 状态,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
取消请求
第二个命令允许乘客或司机取消请求。请记住,乘客和司机都有权取消请求。
|
|
此命令与第一个命令的唯一区别是最后一个参数是 cancel,而在第一个命令中,最后一个参数是 request。
当乘客取消请求时,你应该只取消该请求;当司机取消请求时,你也应该取消该请求,但同时找到另一个最近的司机为这个乘客服务。
只有处于前往接客地点状态的司机或其司机正在前往接客地点的乘客才能取消请求。
当运行此第二个命令时,
- 如果从命令行提供的电话号码与任何乘客的电话号码都不匹配,并且也不与任何司机的电话号码匹配,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
- 如果发出取消请求的是一个状态不是 “Driver_on_the_way” 的乘客,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
- 如果发出取消请求的是一个状态不是 “On_the_way_to_pickup” 的司机,则你的程序应该在 output0.txt 文件中打印以下消息:
|
|
- 如果发出取消请求的是一个处于 “Driver_on_the_way” 状态的乘客,则
4.1 在 output0.txt 文件中打印以下消息:
|
|
4.2 在 output1.txt 文件中打印更新后的 drivers.txt 版本:司机的状态应从 “On_the_way_to_pickup” 更改为 “Available”,并且该司机的最后三个字段应重置为 null,这意味着这个司机现在不再与任何乘客关联。
4.3 在 output2.txt 文件中打印更新后的 riders.txt 版本:乘客的状态应从 “Driver_on_the_way” 更改为 “Ready_to_request”,并且该乘客的最后三个字段应重置为 null,这意味着没有司机现在与这个乘客关联。
- 如果发出取消请求的是一个处于 “On_the_way_to_pickup” 状态的司机,则
5.1 在 output0.txt 文件中打印以下消息:
|
|
用司机的名字替换 Edward。用乘客的名字替换 Angela,用乘客偏好的车辆类型替换 Standard。用接客地点名称替换 The_Met_Cloisters,用目的地名称替换 Brooklyn_Navy_Yard。用新司机的名字替换 Robert。用新司机的评分替换 3.2。用新司机距离乘客的距离替换 2.1。
5.2 在 output1.txt 文件中打印更新后的 drivers.txt 版本:旧司机的状态应从 “On_the_way_to_pickup” 更改为 “Available”。应该分配一个新的司机,并且该新司机的状态也应相应地更新。同时,旧司机不再与这个乘客关联,而新的司机现在与这个乘客关联。
5.3 在 output2.txt 文件中打印更新后的 riders.txt 版本:乘客现在应该与新的司机关联。
基于 Haversine 公式的距离计算
在查找司机时,你必须始终找到最近的车辆类型匹配乘客偏好的司机。当找到最接近的司机后,你也需要打印该司机和乘客之间的距离。因此,你需要一种方法来计算两个坐标之间的距离,在这个任务中,我们将使用 Haversine 公式,并且使用 Haversine 公式的代码如下:
|
|
此函数接受四个参数,即两个地理位置的纬度和经度,并返回这两个位置之间的距离(单位:英里)。该函数调用了几个数学库函数,因此你需要包含 cmath 库:
|
|
包含防护
如果你编写了多个类,在编译时可能会遇到奇怪的编译错误。这可能是由于包括你的类文件的问题,可以通过以下方式解决:对于名为 myclass.h 的头文件,在该头文件顶部添加这两行:
|
|
并在 .h 文件底部添加这一行:
|
|
这种技术称为“包含防护”。包含防护确保编译器只会处理一次头文件,无论它被包括多少次。
常见问题
- Q: 乘客请求的车辆类型是否是严格要求?还是只是一个偏好。如果一个乘客请求 Economy 车型但没有可用的 Economy 司机,而有其他车型的司机,我们应该输出找不到司机,还是匹配最近的不同车型司机?
A: 这是一个严格的要求。不要为乘客选择不同的车型。
- Q: 输出的距离精度是多少?是保留一位小数吗?是否需要四舍五入或直接截断?
A: 与 Uber 相同。保留一位小数。直接截断即可。例如,如果距离是 11.4571 英里,则应输出为 11.4 英里,而不是 11.5 英里。
程序要求及提交细节
在这个作业中,你必须使用 vector 来存储所有司机,并使用 vector 来存储所有乘客。你不允许使用我们尚未学习的数据结构,特别是 std::list。你的程序应该涉及至少两个类的定义,每个类都有自己的 .h 和 .cpp 文件。
在设计和实现程序时,请采用良好的编程风格:将代码组织成函数,不要把所有的代码都放在 main 函数中!请务必阅读 家庭作业政策 以完善你的解决方案。请务必创建新的测试用例来完全调试程序,并不要忘记注释代码!使用提供的模板 README.txt 文件来记录你想评分者阅读的笔记。 你必须独立完成这个作业,如在 合作政策及学术诚信 页面中所述。如果你与任何人讨论过问题或错误信息等,请在 README.txt 文件中列出他们的名字。
截止日期: 2025 年 1 月 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)
- 数据表示 (必须使用向量实现) (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 - 嵌套列表、网格和路径规划