1816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko/*
2816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Copyright (C) 2015 The Android Open Source Project
3816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *
4816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Licensed under the Apache License, Version 2.0 (the "License");
5816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * you may not use this file except in compliance with the License.
6816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * You may obtain a copy of the License at
7816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *
8816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *      http://www.apache.org/licenses/LICENSE-2.0
9816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *
10816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Unless required by applicable law or agreed to in writing, software
11816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * distributed under the License is distributed on an "AS IS" BASIS,
12816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * See the License for the specific language governing permissions and
14816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * limitations under the License.
15816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */
16816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
17816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkopackage com.android.tv.recommendation;
18816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
192e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalkoimport android.support.annotation.Nullable;
20816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport android.support.annotation.VisibleForTesting;
212e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalkoimport android.text.TextUtils;
22816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
23816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport com.android.tv.data.Program;
24816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
25816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.text.BreakIterator;
26816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.ArrayList;
27816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.Calendar;
28816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.List;
29816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.concurrent.TimeUnit;
30816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
31816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkopublic class RoutineWatchEvaluator extends Recommender.Evaluator {
32816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    // TODO: test and refine constant values in WatchedProgramRecommender in order to
33816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    // improve the performance of this recommender.
34816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final double REQUIRED_MIN_SCORE = 0.15;
35816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @VisibleForTesting
36816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    static final double MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK = 0.7;
37816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final double TITLE_MATCH_WEIGHT = 0.5;
38816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final double TIME_MATCH_WEIGHT = 1 - TITLE_MATCH_WEIGHT;
39816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final long DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(14);
40816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static final long MAX_DIFF_MS_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(56);
41816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
42816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @Override
43816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    public double evaluateChannel(long channelId) {
44816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        ChannelRecord cr = getRecommender().getChannelRecord(channelId);
45816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (cr == null) {
46816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return NOT_RECOMMENDED;
47816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
48816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
49816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        Program currentProgram = cr.getCurrentProgram();
50816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (currentProgram == null) {
51816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return NOT_RECOMMENDED;
52816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
53816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
54816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        WatchedProgram[] watchHistory = cr.getWatchHistory();
55816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (watchHistory.length < 1) {
56816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return NOT_RECOMMENDED;
57816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
58816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
59816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        Program watchedProgram = watchHistory[watchHistory.length - 1].getProgram();
60816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        long startTimeDiffMsWithCurrentProgram = currentProgram.getStartTimeUtcMillis()
61816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                - watchedProgram.getStartTimeUtcMillis();
62816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (startTimeDiffMsWithCurrentProgram >= MAX_DIFF_MS_FOR_OLD_PROGRAM) {
63816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return NOT_RECOMMENDED;
64816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
65816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
66816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double maxScore = NOT_RECOMMENDED;
67816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        long watchedDurationMs = watchHistory[watchHistory.length - 1].getWatchedDurationMs();
68816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        for (int i = watchHistory.length - 2; i >= 0; --i) {
69816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            if (watchedProgram.getStartTimeUtcMillis()
70816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    == watchHistory[i].getProgram().getStartTimeUtcMillis()) {
71816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                watchedDurationMs += watchHistory[i].getWatchedDurationMs();
72816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            } else {
73816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                double score = calculateRoutineWatchScore(
74816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                        currentProgram, watchedProgram, watchedDurationMs);
75816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                if (score >= REQUIRED_MIN_SCORE && score > maxScore) {
76816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    maxScore = score;
77816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                }
78816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                watchedProgram = watchHistory[i].getProgram();
79816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                watchedDurationMs = watchHistory[i].getWatchedDurationMs();
80816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                startTimeDiffMsWithCurrentProgram = currentProgram.getStartTimeUtcMillis()
81816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                        - watchedProgram.getStartTimeUtcMillis();
82816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                if (startTimeDiffMsWithCurrentProgram >= MAX_DIFF_MS_FOR_OLD_PROGRAM) {
83816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    return maxScore;
84816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                }
85816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
86816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
87816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double score = calculateRoutineWatchScore(
88816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                currentProgram, watchedProgram, watchedDurationMs);
89816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (score >= REQUIRED_MIN_SCORE && score > maxScore) {
90816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            maxScore = score;
91816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
92816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return maxScore;
93816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
94816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
952e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    private static double calculateRoutineWatchScore(Program currentProgram, Program watchedProgram,
962e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko            long watchedDurationMs) {
97816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double timeMatchScore = calculateTimeMatchScore(currentProgram, watchedProgram);
98816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double titleMatchScore = calculateTitleMatchScore(
99816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                currentProgram.getTitle(), watchedProgram.getTitle());
100816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double watchDurationScore = calculateWatchDurationScore(watchedProgram, watchedDurationMs);
101816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        long diffMs = currentProgram.getStartTimeUtcMillis()
102816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                - watchedProgram.getStartTimeUtcMillis();
103816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double multiplierForOldProgram = (diffMs < MAX_DIFF_MS_FOR_OLD_PROGRAM)
104816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                ? 1.0 - (double) Math.max(diffMs - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM, 0)
105816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                        / (MAX_DIFF_MS_FOR_OLD_PROGRAM - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM)
106816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                : 0.0;
107816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return (titleMatchScore * TITLE_MATCH_WEIGHT + timeMatchScore * TIME_MATCH_WEIGHT)
108816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                * watchDurationScore * multiplierForOldProgram;
109816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
110816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
1112e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    @VisibleForTesting
1122e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    static double calculateTitleMatchScore(@Nullable String title1, @Nullable String title2) {
1132e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko        if (TextUtils.isEmpty(title1) || TextUtils.isEmpty(title2)) {
1142e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko            return 0;
1152e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko        }
116816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        List<String> wordList1 = splitTextToWords(title1);
117816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        List<String> wordList2 = splitTextToWords(title2);
1182e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko        if (wordList1.isEmpty() || wordList2.isEmpty()) {
1192e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko            return 0;
1202e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko        }
121816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        int maxMatchedWordSeqLen = calculateMaximumMatchedWordSequenceLength(
122816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                wordList1, wordList2);
123816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
124816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        // F-measure score
125816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double precision = (double) maxMatchedWordSeqLen / wordList1.size();
126816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double recall = (double) maxMatchedWordSeqLen / wordList2.size();
127816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return 2.0 * precision * recall / (precision + recall);
128816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
129816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
130816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @VisibleForTesting
1312e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    static int calculateMaximumMatchedWordSequenceLength(List<String> toSearchWords,
1322e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko            List<String> toMatchWords) {
133816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        int[] matchedWordSeqLen = new int[toMatchWords.size()];
134816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        int maxMatchedWordSeqLen = 0;
135816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        for (String word : toSearchWords) {
136816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            for (int j = toMatchWords.size() - 1; j >= 0; --j) {
137816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                if (word.equals(toMatchWords.get(j))) {
138816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    matchedWordSeqLen[j] = j > 0 ? matchedWordSeqLen[j - 1] + 1 : 1;
139816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                } else {
140816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    maxMatchedWordSeqLen = Math.max(maxMatchedWordSeqLen, matchedWordSeqLen[j]);
141816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    matchedWordSeqLen[j] = 0;
142816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                }
143816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
144816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
145816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        for (int len : matchedWordSeqLen) {
146816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            maxMatchedWordSeqLen = Math.max(maxMatchedWordSeqLen, len);
147816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
148816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
149816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return maxMatchedWordSeqLen;
150816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
151816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
1522e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    private static double calculateTimeMatchScore(Program p1, Program p2) {
153816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        ProgramTime t1 = ProgramTime.createFromProgram(p1);
154816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        ProgramTime t2 = ProgramTime.createFromProgram(p2);
155816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
156816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double dupTimeScore = calculateOverlappedIntervalScore(t1, t2);
157816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
158816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        // F-measure score
159816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double precision = dupTimeScore / (t1.endTimeOfDayInSec - t1.startTimeOfDayInSec);
160816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double recall = dupTimeScore / (t2.endTimeOfDayInSec - t2.startTimeOfDayInSec);
161816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return 2.0 * precision * recall / (precision + recall);
162816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
163816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
164816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @VisibleForTesting
1652e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    static double calculateOverlappedIntervalScore(ProgramTime t1, ProgramTime t2) {
166816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (t1.dayChanged && !t2.dayChanged) {
167816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            // Swap two values.
168816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return calculateOverlappedIntervalScore(t2, t1);
169816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
170816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
171816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        boolean sameDay = false;
172816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        // Handle cases like (00:00 - 02:00) - (01:00 - 03:00) or (22:00 - 25:00) - (23:00 - 26:00).
173816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        double score = Math.max(0, Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec)
174816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                - Math.max(t1.startTimeOfDayInSec, t2.startTimeOfDayInSec));
175816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (score > 0) {
176816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            sameDay = (t1.weekDay == t2.weekDay);
177816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        } else if (t1.dayChanged != t2.dayChanged) {
178816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            // To handle cases like t1 : (00:00 - 01:00) and t2 : (23:00 - 25:00).
179816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            score = Math.max(0, Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec - 24 * 60 * 60)
180816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    - t1.startTimeOfDayInSec);
181816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            // Same day if next day of t2's start day equals to t1's start day. (1 <= weekDay <= 7)
182816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            sameDay = (t1.weekDay == ((t2.weekDay % 7) + 1));
183816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
184816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
185816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        if (!sameDay) {
186816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            score *= MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK;
187816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
188816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return score;
189816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
190816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
1912e1279b8bbe0603fb4399b25b73121bed5953c46Nick Chalko    private static double calculateWatchDurationScore(Program program, long durationMs) {
192816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return (double) durationMs
193816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                / (program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis());
194816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
195816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
196816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @VisibleForTesting
197816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    static int getTimeOfDayInSec(Calendar time) {
198816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return time.get(Calendar.HOUR_OF_DAY) * 60 * 60
199816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                + time.get(Calendar.MINUTE) * 60
200816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                + time.get(Calendar.SECOND);
201816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
202816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
203816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @VisibleForTesting
204816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    static List<String> splitTextToWords(String text) {
205816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        List<String> wordList = new ArrayList<>();
206816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        BreakIterator boundary = BreakIterator.getWordInstance();
207816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        boundary.setText(text);
208816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        int start = boundary.first();
209816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        for (int end = boundary.next(); end != BreakIterator.DONE;
210816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                start = end, end = boundary.next()) {
211816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            String word = text.substring(start, end);
212816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            if (Character.isLetterOrDigit(word.charAt(0))) {
213816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                wordList.add(word);
214816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            }
215816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
216816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return wordList;
217816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
218816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
219816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    @VisibleForTesting
220816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    static class ProgramTime {
221816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        final int startTimeOfDayInSec;
222816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        final int endTimeOfDayInSec;
223816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        final int weekDay;
224816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        final boolean dayChanged;
225816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
226816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        public static ProgramTime createFromProgram(Program p) {
227816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            Calendar time = Calendar.getInstance();
228816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
229816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            time.setTimeInMillis(p.getStartTimeUtcMillis());
230816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            int weekDay = time.get(Calendar.DAY_OF_WEEK);
231816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            int startTimeOfDayInSec = getTimeOfDayInSec(time);
232816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
233816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            time.setTimeInMillis(p.getEndTimeUtcMillis());
234816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            boolean dayChanged = (weekDay != time.get(Calendar.DAY_OF_WEEK));
235816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            // Set maximum program duration time to 12 hours.
236816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            int endTimeOfDayInSec = startTimeOfDayInSec +
237816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                    (int) Math.min(p.getEndTimeUtcMillis() - p.getStartTimeUtcMillis(),
238816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                            TimeUnit.HOURS.toMillis(12)) / 1000;
239816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
240816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            return new ProgramTime(startTimeOfDayInSec, endTimeOfDayInSec, weekDay, dayChanged);
241816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
242816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
243816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        private ProgramTime(int startTimeOfDayInSec, int endTimeOfDayInSec, int weekDay,
244816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                boolean dayChanged) {
245816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            this.startTimeOfDayInSec = startTimeOfDayInSec;
246816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            this.endTimeOfDayInSec = endTimeOfDayInSec;
247816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            this.weekDay = weekDay;
248816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko            this.dayChanged = dayChanged;
249816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        }
250816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
251816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko}
252