首页 > 其他分享 >电信计费系列

电信计费系列

时间:2022-12-10 10:56:40浏览次数:37  
标签:系列 String private return records 计费 new 电信 public

前言

这次开始换了一个类型,所有的都重新开始(大概是前面的点线型的题目迭代的有点复杂了, 基本框架已经固定, 剩下的以算法为主)。这次新开的题目如果按照面向过程来写就十分的简单。但是由于我们是JAVA课, 题目里面直接塞了一个类图。按照类图来写还是比较简单的。

设计

还是先看一下题目。

实现一个简单的电信计费程序:
输入信息统一格式为

[utm]-.*

其中 u 代表开户信息, t 电话通信, m 代表短信通信
具体的输入格式如下

  1. 逐行输入南昌市用户开户的信息,每行一个用户

    格式:u-号码 计费类型 (计费类型包括:0-座机 1-手机实时计费 3-短信计费)

    例如:u-079186300001 0

  2. 逐行输入本月某些用户的通讯信息,通讯信息格式

  • 座机呼叫座机

    t-主叫号码 接听号码 起始时间 结束时间

    t-079186330022 058686330022 2022.1.3 10:00:25 2022.1.3 10:05:11
  • 手机用户

    t-079186330022 13307912264 020 2022.1.3
    10:00:25 2022.1.3 10:05:11
  • 2.3 短信

    m-18907910010 13305862264 welcome to jiangxi.

    m-13305862264 18907910010 thank you.

计算方式

  1. 固定电话计费
  • 月租20元,接电话免费
  • 市内拨打电话0.1元/分钟
  • 省内长途0.3元/分钟
  • 国内长途拨打0.6元/分钟。不足一分钟按一分钟计。
  1. 手机计费
  • 月租15元,市内省内接电话均免费
  • 市内拨打市内电话0.1元/分钟
  • 市内拨打省内电话0.2元/分钟
  • 市内拨打省外电话0.3元/分钟
  • 省内漫游打电话0.3元/分钟
  • 省外漫游接听0.3元/分钟
  • 省外漫游拨打0.6元/分钟

    注:被叫电话属于市内、省内还是国内由被叫电话的接听地点区号决定,比如以下案例中,南昌市手机用户13307912264在区号为020的广州接听了电话,主叫号码应被计算为拨打了一个省外长途,同时,手机用户13307912264也要被计算省外接听漫游费。
  1. 短信计费
  • 接收短信免费,发送短信0.1元/条,超过3条0.2元/条,超过5条0.3元/条。
  • 如果一次发送短信的字符数量超过10个,按每10个字符一条短信进行计算

注意事项

  1. 以上两类信息,先输入所有开户信息,再输入所有通讯信息,最后一行以“end”结束。
  2. 时间必须符合"yyyy.MM.dd HH:mm:ss"格式。
  3. 8座机号码除区号外由是7-8位数字组成。
  4. 座机号码由区号和电话号码拼接而成,电话号码包含7-8位数字,区号最高位是0。手机号码由11位数字构成,最高位是1。
  5. 南昌市的区号:0791,江西省内各地市区号包括:0790~0799以及0701。

题目中给出的类图如下
img
img
img

上面的类图整体上描述的还是比较清晰的
首先有一个User类, 其中UserRecords作为记账本。balance记录现有的话费。number记录电话号码。
然后再UserRecords 里面记录 CallRecord, CallRecord作为父类引出各种通信方式的类。ChargeMode记录收费方式。ChargeMode里面又有chargeRule作为不同的收费规则。所以有上面的类图做参考。写起来还是比较轻松的。关键是各种边界条件的判断。

我们可以看到输入的东西是一个非常复杂的字符串。特别是加入了手机之后。因此在上面类图的基础上, 有比较自己新建类去解析输入的内容于是我还增加了一个 Parser类用来解析输入的内容。类图如下

img

实现

先看主函数

public class Main {
    private static final
    HashMap<String, User> users = new HashMap<String, User>();

    private static final Parser parser = new Parser();

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        while(true) {
            try {
                String line = sc.nextLine();
                if (line.equals("end")) break;
                if (!line.matches("[utm]-[\\s0-9:.]*")) continue;
                // if (!line.matches("[utm]-.*")) continue;
                if (line.charAt(0) == 'u') {        // 注册用户
                    // 解析出 user
                    User user = parser.parserUser(line.substring(2));
                    users.put(user.getNumber(), user);
                } else if (line.charAt(0) == 't') { // 打电话用户
                    CallRecords records =           // 解析出 records
                            parser.parserRecordsNew(line.substring(2));
                    String callNumber = records.getCallNumber();
                    String ansNumber = records.getAnswerNumber();
                    if(users.containsKey(callNumber))   // 分情况加入Records
                        users.get(callNumber)
                                .getUserRecords().addCallRecords(records);
                    if(users.containsKey(ansNumber))
                        users.get(ansNumber)
                                .getUserRecords().addAnswerRecords(records);
                } else if(line.charAt(0) == 'm') {  // 短信用户
                    // 解析出 records
                    MessageRecords records = parser.parserMessageRecords(line
                            .substring(2));
                    String callNumber = records.getCallNumber();
                    if(users.containsKey(callNumber))
                        users.get(callNumber)
                                .getUserRecords().addMessageRecords(records);
                }
            } catch (Exception ignore) {}
        }
        // 对用户进行排序
        List<String> list = new ArrayList<>(users.keySet());
        Collections.sort(list);
        // 算出月租和总的费用
        for(String s : list) {
            User user = users.get(s);
            double chargerRuleCost = user.getChargerMode().getMonthlyRent();
            double realCost = user.getChargerMode()
                    .computeCost(user.getUserRecords());
            user.setBalance(user.getBalance() - chargerRuleCost - realCost);
            double balance = valueOf(user.getBalance(), 2);
            realCost =  valueOf(realCost, 2);
            System.out.println(user.getNumber() + " " + realCost + " " + balance);
        }
    }

    // 保留两位小数
    public static double valueOf(double d, int how) {
        return new BigDecimal(d)
                .setScale(how, RoundingMode.HALF_UP).doubleValue();
    }
}

主函数有点复杂,上面做得事情主要是根据信息进行分类然后调用Parser类中的方法计算出对应得Records。
然后再统一得计算 cost, 这里得cost实际上不应该在这里算, 由于一开始写得时候没看懂类图,所以这样写了。但是不影响大的流程。
紧接着就是 parser 类
这个类比较复杂

public class Parser {
    private static final HashMap<String, String>
            chargerModeMap = new HashMap<>() {{
        put("0", "LandlinePhoneCharger");
        put("1", "SmartPhoneCharger");
        put("3", "MessageCharger");
    }};
    // 判断时间得正则, 必须要先编译,如果直接使用 string.matches(...) 都会编译一遍极易超时
    private final Pattern pattern = Pattern.compile("^((([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})\\.((([13578]|1[02])\\.([1-9]|[12][0-9]|3[01]))|(([469]|11)\\.([1-9]|[12][0-9]|30))|(2\\.([1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})([48]|[2468][048]|[13579][26])|(([48]|[2468][048]|[3579][26])00))\\.02\\.29))\\s+([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$");

    // 特殊的区号
    private final HashSet<String> specialAreaCode = new HashSet<>() {{
        add("010");add("022");add("021");add("023");
        add("852");add("853");add("025");add("024");
        add("029");add("020");add("028");add("027");
    }};

    // 解析用户
    public User parserUser(String s) throws Exception {
        if(s.contains("  ") || s.startsWith(" ") || s.endsWith(" "))
            throw new RuntimeException("parser error");
        Scanner sc = new Scanner(s);
        String number = sc.next();
        String chargerMode = sc.next();
        if(isIllegalNumber(number))
            throw new RuntimeException("parser error");
        if(sc.hasNext())
            throw new RuntimeException("parser error");
        return userBuilder(number, chargerMode);
    }

    // 构建用户
    private static User userBuilder(String number, String chargerMode)
            throws Exception {
        if(!chargerModeMap.containsKey(chargerMode))
            return null;
        ChargerMode mode =      // 使用 反射 构建 mode
                (ChargerMode) Class.forName(chargerModeMap.get(chargerMode))
                .getConstructor().newInstance();
        return new User(number, mode);
    }

    // 非法固定电话
    private boolean isIllegalNumber(String number) {
        return !number.matches("^\\d{10,12}");
    }

    // 非法手机电话
    private boolean isIllegalSmartPhoneNumber(String number) {
        return !number.matches("\\d{11}");
    }

    // 非法区号
    private boolean isIllegalAreaCode(String ansAreaCode) {
        return !ansAreaCode.matches("\\d{3,4}");
    }

    // 非法时间
    private boolean isIllegalTime(String time) {
        return !pattern.matcher(time).matches();
    }

    // 解析手机或固定电话记录
    CallRecords parserRecordsNew(String s) throws ParseException {
        if(s.contains("  ") || s.startsWith(" ") || s.endsWith(" "))
            throw new RuntimeException("parser error");
        CallRecords records = new CallRecords();
        Scanner sc = new Scanner(s);
        boolean b = true;
        int i = 0;
        do {
            String tmp = sc.next();
            if (tmp.charAt(0) == '0') {     // 座机
                parserFixedNumber(tmp, records, b);
            } else if (!isIllegalSmartPhoneNumber(tmp)) { // 手机
                parseSmartNumber(tmp, sc.next(), records, b);
            } else
                throw new RuntimeException("parser error");
            b = false;
            ++i;
        } while(i < 2);
        // 时间
        parserData(sc, records);
        if(sc.hasNext())
            throw new RuntimeException("parser error");
        return records;
    }

    // 解析手机电话
    private void parseSmartNumber(String tmp,
                                  String next,
                                  CallRecords records,
                                  boolean b) {
        if(isIllegalAreaCode(next))
            throw new RuntimeException("parser error");

        if(b) {
            records.setCallNumber(tmp);
            records.setCallAreaCode(next);
            return;
        }
        records.setAnswerNumber(tmp);
        records.setAnswerAreaCode(next);
    }

    // 解析固定电话
    private void parserFixedNumber(String tmp, CallRecords records, boolean b) {
        if(isIllegalNumber(tmp))
            throw new RuntimeException("parser error");
        int end = specialAreaCode.contains(tmp.substring(0, 3)) ? 3 : 4;
        String code = tmp.substring(0, end);
        if(b) {
            records.setCallAreaCode(code);
            records.setCallNumber(tmp);
            return;
        }
        records.setAnswerAreaCode(code);
        records.setAnswerNumber(tmp);
    }

    // 解析日期
    private void parserData(Scanner sc, CallRecords records) throws ParseException {
        String startDate = sc.next() + " " + sc.next();
        String endDate = sc.next() + " " + sc.next();
        if(isIllegalTime(startDate) || isIllegalTime(endDate))
            throw new RuntimeException("parser error");

        SimpleDateFormat dateFormat =
                new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
        Date start = dateFormat.parse(startDate);
        Date end = dateFormat.parse(endDate);
        records.setStart(start);
        records.setEnd(end);
    }

    // 解析信息
    public MessageRecords parserMessageRecords(String s) {
        if(isIllegalMessage(s))
            throw new RuntimeException("parser error");
        Scanner sc = new Scanner(s);
        String callNumber = sc.next();
        String answerNumber = sc.next();
        String message = sc.nextLine();
        return new MessageRecords(callNumber, answerNumber, message);
    }

    // 非法信息
    private boolean isIllegalMessage(String s) {
        return !s.matches("^\\d{11}\\s\\d{11}\\s.*$");
    }
}

在写parser类的时候一开始我是想写一个状态机, 但是输入的文本确实没有什么很明显的规律(个人觉得), 反正就我的水平还写不出一个比较好的状态机去解析这个文本。所以总体上来看,这个parser类显得有点乱。

然后是UserRecord类

public class UserRecord{
    private final ArrayList<CallRecords> callInCityRecords = new ArrayList<>();
    private final ArrayList<CallRecords> callInProvinceRecords = new ArrayList<>();
    private final ArrayList<CallRecords> callInLandRecords = new ArrayList<>();
    private final ArrayList<CallRecords> answerInCityRecords = new ArrayList<>();
    private final ArrayList<CallRecords> answerInProvinceRecords = new ArrayList<>();
    private final ArrayList<CallRecords> answerInLandRecords = new ArrayList<>();
    private final ArrayList<MessageRecords> messageRecords = new ArrayList<>();
    public UserRecord() {
    }

    public ArrayList<MessageRecords> getMessageRecords() {
        return messageRecords;
    }

    public ArrayList<CallRecords> getCallInCityRecords() {
        return callInCityRecords;
    }

    public ArrayList<CallRecords> getCallInProvinceRecords() {
        return callInProvinceRecords;
    }

    public ArrayList<CallRecords> getCallInLandRecords() {
        return callInLandRecords;
    }

    public ArrayList<CallRecords> getAnswerInCityRecords() {
        return answerInCityRecords;
    }

    public ArrayList<CallRecords> getAnswerInProvinceRecords() {
        return answerInProvinceRecords;
    }

    public ArrayList<CallRecords> getAnswerInLandRecords() {
        return answerInLandRecords;
    }

    public void addCallRecords(CallRecords record) {
        String areaCode = record.getCallAreaCode();
        if(areaCode.equals("0791"))
            callInCityRecords.add(record);
        else if(areaCode.matches("079[0-9]|0701"))
            callInProvinceRecords.add(record);
        else callInLandRecords.add(record);
    }

    public void addAnswerRecords(CallRecords record) {
        String areaCode = record.getAnswerAreaCode();
        if(areaCode.equals("0791"))
            answerInCityRecords.add(record);
        else if(areaCode.matches("079[0-9]|0701"))
            answerInProvinceRecords.add(record);
        else answerInLandRecords.add(record);
    }

    public void addMessageRecords(MessageRecords records) {
        messageRecords.add(records);
    }
}

这个类其实就是把各种各样得Records加进来, 然后设置get和set方法。注释就不写了。唯一需要注意得是在addAnswerRecords方法中要根据区号加入不同得Records。而区号可能是3为也可能是4位。但是上面统一按照4位处理了。

然后是CommunicationRecords以及它的子类

// file CommunicationRecords.java
public abstract class CommunicationRecords {
    public String callNumber;
    private String answerNumber;

    public String getCallNumber() {
        return callNumber;
    }

    public String getAnswerNumber() {
        return answerNumber;
    }

    public void setCallNumber(String callNumber) {
        this.callNumber = callNumber;
    }

    public void setAnswerNumber(String answerNumber) {
        this.answerNumber = answerNumber;
    }

    public CommunicationRecords(String callNumber, String answerNumber) {
        this.callNumber = callNumber;
        this.answerNumber = answerNumber;
    }

    public CommunicationRecords() {}
}

// file CallRecords.java
public class CallRecords extends CommunicationRecords {
    protected Date start;
    protected Date end;

    protected String callAreaCode;
    protected String answerAreaCode;

    public CallRecords() {
    }
    public CallRecords(Date start, Date end,
                       String startNumber, String endNumber) {
        super(startNumber, endNumber);
        this.callAreaCode = startNumber.substring(0, 4);
        this.answerAreaCode = endNumber.substring(0, 4);
        this.start = start;
        this.end = end;
    }

    public CallRecords(String callNumber, String answerNumber, 
            Date start, Date end, String callAreaCode,
            String answerAreaCode) {
        super(callNumber, answerNumber);
        this.start = start;
        this.end = end;
        this.callAreaCode = callAreaCode;
        this.answerAreaCode = answerAreaCode;
    }

    public Date getEnd() {
        return end;
    }

    public Date getStart() {
        return start;
    }

    // 获得打电话得时间, 注意这里不足一分钟向上取整
    public double getTime() {
        return Math.ceil((end.getTime() - start.getTime())/ 60000d);
    }

    public String getCallAreaCode() {
        return callAreaCode;
    }

    public String getAnswerAreaCode() {
        return answerAreaCode;
    }

    public void setCallAreaCode(String callAreaCode) {
        this.callAreaCode = callAreaCode;
    }

    public void setAnswerAreaCode(String answerAreaCode) {
        this.answerAreaCode = answerAreaCode;
    }

    public void setStart(Date start) {
        this.start = start;
    }

    public void setEnd(Date end) {
        this.end = end;
    }
}

// file MessageRecords.java
public class MessageRecords extends CommunicationRecords {
    private final String message;
    public MessageRecords(String callNumber, String answerNumber, String message) {
        super(callNumber, answerNumber);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

这个两个类几乎都是get和set方法, 注释都没什么写得。。唯一得就是计算时间的时候需要向上取整

最后的ChargeRule及其子类


// file ChargerRule.java
public abstract class ChargerRule {
}

// file LandlinePhoneCharger.java
public class LandlinePhoneCharger extends ChargerMode {
    public LandlinePhoneCharger() {
        super(20);
    }
    @Override
    public double computeCost(UserRecord userRecord) {
        double cost = 0;
        for (CallRecords re : userRecord.getCallInCityRecords()) {
            double time = re.getTime();
            String ansCode = re.getAnswerAreaCode();
            if(isCityCode(ansCode))
                cost += .1 * time;
            else if(isProvinceCode(ansCode))
                cost += .3 * time;
            else cost += .6 * time;
        }
        for (CallRecords re : userRecord.getCallInLandRecords()) {
            double time = re.getTime();
            cost += .6 * time;
        }
        for(CallRecords re : userRecord.getCallInProvinceRecords()) {
            double time = re.getTime();
            cost += .3 * time;
        }
        return cost;
    }
}

// file SmartPhoneCharger.java
public class SmartPhoneCharger extends ChargerMode {
    public SmartPhoneCharger() {
        super(15);
    }
    @Override
    public double computeCost(UserRecord userRecord) {
        double cost = 0;
        for(CallRecords records : userRecord.getCallInCityRecords()) {
            double time = records.getTime();
            String ansCode = records.getAnswerAreaCode();
            if(isCityCode(ansCode))
                cost += 0.1 * time;
            else if(isProvinceCode(ansCode))
                cost += 0.2 * time;
            else
                cost += 0.3 * time;
        }
        for (CallRecords records : userRecord.getCallInProvinceRecords()) {
            double time = records.getTime();
            cost += 0.3 * time;
        }
        for (CallRecords records : userRecord.getCallInLandRecords()) {
            double time = records.getTime();
            cost += 0.6 * time;
        }
        for (CallRecords records : userRecord.getAnswerInLandRecords()) {
            double time = records.getTime();
            cost += 0.3 * time;
        }
        return cost;
    }
}

// file MessageCharger.java
public class MessageCharger extends ChargerMode {
    public MessageCharger() {
        super(0);
    }

    @Override
    public double computeCost(UserRecord userRecord) {
        double cost = 0;
        int msgCnt = 0;
        for (MessageRecords record : userRecord.getMessageRecords()) {
            //int msgCnt = record.getMessage().length() / 10 + 1;
            int len = record.getMessage().length();
            if (len <= 10) ++msgCnt;
            else {
                msgCnt += len / 10;
                if (len % 10 != 0)
                    ++msgCnt;
            }

        }
        if (msgCnt > 5) {
            cost += msgCnt * .3;
        } else if (msgCnt > 3) {
            cost += msgCnt * .2;
        } else {
            cost += msgCnt * .1;
        }
        return cost;
    }
}

我这里上面得*Charger类在computeCost方法中已经把ChargerMode类作用实现了。因为一开始没有看懂老师给出来得类图到底是怎么运行得, 所以就自己写了。所以实际上到这里就可以计算出通过所有得题目了。

总结

这几次PTA老师给了类图, 但是面对这个类图确实还是有点吃力得。一开始面对这个类图是不太理解为什么要搞这么复杂。而且有点不理解设置这个类得含义。后面自己动手去实现得时候才慢慢发现确实这样写可以屏蔽其他的细节, 使得写出来的代码就像积木一样一个一个拼接在一起。 就正如上课讲得需要将每一个变化抽象出一个类。然后面向抽象编程。面向对象的本质和初衷就是抽象。一个好的抽象应该尽可能的使得程序归一化。所谓归一化就是按我的理解就是尽可能得定义出一个统一的接口在屏蔽底层得细节得同时, 使得不同对象都可以完成一个统一的逻辑, 也就是多态。面向对象把每一个变化抽象成一个接口, 就可以成功得做到归一化, 也就是一个好的抽象。这也提醒我使用面向对象得思想得时候一定要做出一个好的抽象,不好得抽象只会使得程序越来越乱。归根到底,也就是上课讲得那几个原则:单一职责、开闭原则等等。。

标签:系列,String,private,return,records,计费,new,电信,public
From: https://www.cnblogs.com/usersname/p/16945824.html

相关文章