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和高手指教。