从一道Neusoft题中想到的IO和Comparator

    Neusoft内部定期举行考试,给一到题目,限期完成,进行评定,想必每个Neusofter对此都很有体会。话说某年Neusoft某分公司某研发部有这样一道考试题,原题是这样叙述的:

    某公司为其它公司做技术服务,人员按照客户要求出差外派。补贴是在人员出差前预先派发的。需要计算出每个人的补贴数值,并且需要派出日期先后排序,以便于安排进行统一借款并进行补贴的派发。如果派出日期相同,则按照补贴金额从少到多排序。
    按照出差时间长短,补贴的标准是不同的。具体规定是:
    30天以内,每日补贴50元;超出31而在60天以内部分,每日补贴多10元,即60元;超出61而在90天以内部分,每日补贴再多10即70元,……以30日为周期以此类推。
    出差的天数以自然日计算,不需要考虑节假日。
    举例说明:
    张三2010-9-16外派出差,到2010-9-30回到公司,计算出差时间为15天,因为少于30天,出差补贴为50*15=750元。
    李四2010-9-1外派出差,到2010-10-20回到公司,计算出差时间为50天,50*30+60*20=2700元。
    为了方便后期调整出差补贴标准,需要采用config.properties对上面的补贴标准进行配置,程序运行时从C:\test\下读取。
    配置文件的内容为:
base=50
step=10
    给出的输入文件为C:\test\src.txt,每行内容为3部分,姓名 派出日期 释放日期
    其中:每个字段中间以一个空格分隔,日期的形式为2010-9-17。
    结果请写入C:\test\result.txt中,每行内容为5部分:姓名 派出日期 释放日期 出差天数 补助金额。
    其中:每个字段中间以一个空格分隔,日期的形式为2010-9-17(注:月份或日期位数不满2位的,不需要以0补全2位,即2010-9-1不需要输出为2010-09-01);出差天数、金额保留到整数位。
    输入、输出文件编码方式都使用GBK。
    提示:编程过程中,可以使用apache commons包中的api (这个建议与考查的内容无关,至少便于对处理文件关闭进行处理,评分是不会有任何影响)
除以上包以外,请使用j2se5.0或6.0的标准内容。引入其他第3方库并不符合考试要求。


    看完试题,我先忽略Apache的Commons组件吧,既然不参与评分,那还是全部J2SE基础编程吧。整体流程就是从一个文件读取信息,处理这个信息,然后再写到另外一个文件中。简单的IO操作可以完成。要根据配置文件的信息计算,那么就要先加载配置文件,也是简单的IO操作。再者是对出差补贴的计算,简单的数列求和计算,因为是分段函数,那么每个区间点的基数就是数列,而区间内的数值就是一个函数。对结果要求进行条件排序,很自然想到Comparator的使用。还有一个小点就是对日期格式的处理。仅此而已。
    既然要求使用Java,就要使用面向对象的方法来解决,先看看给出的源文件src.txt:

张三 2010-9-17 2010-10-15
李四 2010-9-5 2010-10-30
王五 2010-9-20 2010-11-2
赵六 2010-10-2 2010-10-30
阿童木 2010-10-15 2010-12-31

    那么先创建一个bean来描述一下出差对象吧,按照要求,还要有出差天数和补贴。这个类很简单,如下设计:
import java.util.Date;
public class BusinessTrip {
	private String userName;
	private Date startDate;
	private Date endDate;
	private String days;
	private Integer allowance;
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public Date getStartDate() {
		return startDate;
	}
	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}
	public Date getEndDate() {
		return endDate;
	}
	public void setEndDate(Date endDate) {
		this.endDate = endDate;
	}
	public String getDays() {
		return days;
	}
	public void setDays(String days) {
		this.days = days;
	}
	public Integer getAllowance() {
		return allowance;
	}
	public void setAllowance(Integer allowance) {
		this.allowance = allowance;
	}
}

    字段都很简单,不做任何说明了。只要注意用来排序的字段要用实现了Comparable接口的类,这样才能调用它们的compareTo()方法进行排序。因为要使用Comparator,那么需要一个编写一个Comparator,如下:
import java.util.Comparator;
public class ComparatorStartDate implements Comparator<BusinessTrip> {
	@Override
	public int compare(BusinessTrip trip0, BusinessTrip trip1) {
		// 升序排序
		int flag=trip0.getStartDate().compareTo(trip1.getStartDate());
		if(flag==0){//如果派出日期相同,则按照补贴金额从少到多排序。
			return trip0.getAllowance().compareTo(trip1.getAllowance());
		}else{
			return flag;
		}
	}
}

    这里做个简单说明,要进行排序,那么排序方法类就要实现Comparator接口,定义泛型后在覆盖的方法参数就可以直接使用该类,而用再强转Object了。还要记得一点,对List排序时是对List本身包含元素的排序还是对其中对象的某个属性排序。当然这里我们会用List封装BusinessTrip,那么是对BusinessTrip的某属性排序,所以此处定义泛型是BusinessTrip,而不是List。
    剩下就是计算方法的类了,这里我没有继续分开文件的读取和写入操作,而是一起写入了计算类。我们首先来看配置文件的加载,因为要求要从C盘下某路径下载,只能使用Properties来解决,不能用ResourceBundle了,因为后者直接加载类路径下的配置文件。而对配置文件的加载一般是在静态块内完成比较好,如下:
private static int base;
	private static int step;
	/**
	 * 加载属性配置文件
	 */
	static {
		InputStream is = null;
		Properties prop = new Properties();
		try {
			is = new FileInputStream("C:\\test\\config.properties");
			prop.load(is);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (is != null) {
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		base = Integer.parseInt(prop.getProperty("base"));
		step = Integer.parseInt(prop.getProperty("step"));
	}

    注意IO操作的规范性就行了,下面是加载文件和写入文件的方法,针对此例,做出如下设计:
/**
	 * 从文件中加载信息
	 * 
	 * @param fileName
	 * @return
	 */
	public static List<String> readFromFile(String fileName) {
		final File file = new File(fileName);
		List<String> list = new ArrayList<String>();
		if (file.exists()) {
			BufferedReader input = null;
			try {
				input = new BufferedReader(new InputStreamReader(
						new FileInputStream(file), "GBK"));
				String line = null;
				while ((line = input.readLine()) != null) {
					list.add(line);
				}
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				try {
					if (input != null) {
						input.close();
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return list;
	}

    因为数据是一行一行出现的,那么用List封装比较合适,所以读取时就直接封装到List里面了。下面是写入操作:
/**
	 * 将结果写回文件
	 * 
	 * @param list
	 * @param fileName
	 */
	public static void write2file(List<String> list, String fileName) {
		BufferedOutputStream output = null;
		try {
			output = new BufferedOutputStream(new FileOutputStream(fileName));
			for (int i = 0; i < list.size(); i++) {
				output.write((list.get(i) + "\r\n").getBytes());
			}
			output.flush();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (output != null) {
					output.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

    还是将List写回到文件,这里读取和写入都是使用的String泛型的List,而我们实际要操作一个对象,这是题目要求的,写回时不能改变已有格式和信息。那么主函数内就要分开操作,也就是循环遍历,这个不难,可难在对补贴的算法上。
    我们来看看补贴的计算:不满30天,就是不到一个周期,只是按天数和基本补助额度算,那必须单独算,后面是要加成的。好,从满30天,即一个周期开始,要加成计算了,那么来看看加成的方式:

30~60天:period=1 50*30+(day-30)*60
60~90天:period =2 50*30+30*60+(day-60)*70
90~120天:period =3 50*30+60*30+70*30+(day-90)*80
120~160天:period =4 50*30+60*30+70*30+80*30+(day-120)*90

    可以发现,每个周期内的零碎计算是很好算的,第一周期时比较特殊,这里base是50,step是10,那么不足一个周期的补助是(day - 30) * (base + step),后面周期的则是:(day - 30 * period)* (base + step + step * (period - 1)),那么还需要再分开判断一次。也就是按照period为0,为1,和大于2时计算。
    比较难的是满整周期的算法,可以看出它们的规律,都是30天乘以本周期的加成数。这是个数列,分别是30*(50),30*(50+60),30*(50+60+70),30*(50+60+70+80),30*(50+60+70+80+90),因为第一期内的要提出分开算,那么我们从period=2开始算,30是天数,固定,50是基数,就是base,那么这个从第二项开始的数列就是:2base+10,3base+30,4base+60,5base+100。又可以发现base的系数和周期数相同,变化的则是后面的数值,那么定义一个新数列b2=10,b3=30,b4=60,b5=100,也就是b3=b2+20,b4=b3+30,b5=b4+40,这是个递归数列,通项公式中还有一部分是等差数列。求和很简单了,写出通项公式:bn-1=bn-2+(n-2)*10,bn=bn-1+(n-1)*10。那么使用相加消除法,就可以得到bn=period * step * (period - 1) / 2,这样就解决了补贴算法问题。整个方法如下:
/**
	 * 测试方法
	 * 
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		long starttime = System.currentTimeMillis();
		List<String> list = readFromFile("C:\\src.txt");
		List<BusinessTrip> tripList = new ArrayList<BusinessTrip>();
		DateFormat df = new SimpleDateFormat("yyyy-M-d");
		List<String> resList = new ArrayList<String>();
		// 读取文件信息,拼装成对象
		for (int i = 0; i < list.size(); i++) {
			BusinessTrip trip = new BusinessTrip();
			String item = (String) list.get(i);
			String[] items = item.split(" ");
			trip.setUserName(items[0]);
			trip.setStartDate(df.parse(items[1]));
			trip.setEndDate(df.parse(items[2]));
			tripList.add(trip);
		}
		// 处理对象
		for (int i = 0; i < tripList.size(); i++) {
			BusinessTrip work = tripList.get(i);
			long time = work.getEndDate().getTime()
					- work.getStartDate().getTime();
			int day = (int) (time / (60 * 60 * 24 * 1000)) + 1;
			int period = (int) (day / 30);
			work.setDays(day + "");
			int allance;
			// 核心算法,数列求和是关键
			if (period == 0) {
				allance = base * day;
			} else if (period == 1) {
				allance = 30 * base + (day - 30) * (base + step);
			} else {
				allance = 30
						* (base * period + period * step * (period - 1) / 2)
						+ (day - 30 * period)
						* (base + step + step * (period - 1));
			}
			work.setAllowance(allance);
		}
		// 进行开始日期的比较
		ComparatorStartDate csd = new ComparatorStartDate();
		Collections.sort(tripList, csd);
		for (int i = 0; i < tripList.size(); i++) {
			StringBuffer sb = new StringBuffer();
			BusinessTrip trip = tripList.get(i);
			sb.append(trip.getUserName()).append(" ").append(
					df.format(trip.getStartDate())).append(" ").append(
					df.format(trip.getEndDate())).append(" ").append(
					trip.getDays()).append(" ").append(trip.getAllowance());
			resList.add(sb.toString());
		}
		write2file(resList, "c:\\result.txt");
		long endtime = System.currentTimeMillis();
		System.out.println(endtime - starttime + " ms");
		System.out.println("文件已生成!");
	}

   这样就完成了出差补贴的计算。关键之处应该是数列的计算,而IO,日期操作,属性加载不应该是困难所在。
    说点题外话:Neusoft这种制度还是不错的,时刻提醒大家要注重的地方,数学是基础,IO是基础,不能老是SSH。这种为员工跳槽着想的做法是值得学习的。其他方面我就不评论了,毕竟我不是Neusoft的。
    一家之言,仅供参考,毕竟没有深入理解出题人的意图,这并不是最佳实践,程序执行时间也仅在25-30ms之间(T7200 2.5G WIN7 JDK6_0_14)。欢迎Neusofter和高手指教。

你可能感兴趣的:(编程,C++,c,算法,C#)