UserHistoryForgettingCurveUtils.java revision bcec82de66f52655593dc233346f11468f5077a0
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.text.format.DateUtils;
20import android.util.Log;
21
22public class UserHistoryForgettingCurveUtils {
23    private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName();
24    private static final boolean DEBUG = false;
25    private static final int FC_FREQ_MAX = 127;
26    /* package */ static final int COUNT_MAX = 3;
27    private static final int FC_LEVEL_MAX = 3;
28    /* package */ static final int ELAPSED_TIME_MAX = 15;
29    private static final int ELAPSED_TIME_INTERVAL_HOURS = 6;
30    private static final long ELAPSED_TIME_INTERVAL_MILLIS = ELAPSED_TIME_INTERVAL_HOURS
31            * DateUtils.HOUR_IN_MILLIS;
32    private static final int HALF_LIFE_HOURS = 48;
33    private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1);
34
35    private UserHistoryForgettingCurveUtils() {
36        // This utility class is not publicly instantiable.
37    }
38
39    public static class ForgettingCurveParams {
40        private byte mFc;
41        long mLastTouchedTime = 0;
42        private final boolean mIsValid;
43
44        private void updateLastTouchedTime() {
45            mLastTouchedTime = System.currentTimeMillis();
46        }
47
48        public ForgettingCurveParams(boolean isValid) {
49            this(System.currentTimeMillis(), isValid);
50        }
51
52        private ForgettingCurveParams(long now, boolean isValid) {
53            this(pushCount((byte)0, isValid), now, now, isValid);
54        }
55
56        /** This constructor is called when the user history bigram dictionary is being restored. */
57        public ForgettingCurveParams(int fc, long now, long last) {
58            // All words with level >= 1 had been saved.
59            // Invalid words with level == 0 had been saved.
60            // Valid words words with level == 0 had *not* been saved.
61            this(fc, now, last, fcToLevel((byte)fc) > 0);
62        }
63
64        private ForgettingCurveParams(int fc, long now, long last, boolean isValid) {
65            mIsValid = isValid;
66            mFc = (byte)fc;
67            mLastTouchedTime = last;
68            updateElapsedTime(now);
69        }
70
71        public boolean isValid() {
72            return mIsValid;
73        }
74
75        public byte getFc() {
76            updateElapsedTime(System.currentTimeMillis());
77            return mFc;
78        }
79
80        public int getFrequency() {
81            updateElapsedTime(System.currentTimeMillis());
82            return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
83        }
84
85        public int notifyTypedAgainAndGetFrequency() {
86            updateLastTouchedTime();
87            // TODO: Check whether this word is valid or not
88            mFc = pushCount(mFc, false);
89            return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
90        }
91
92        private void updateElapsedTime(long now) {
93            final int elapsedTimeCount =
94                    (int)((now - mLastTouchedTime) / ELAPSED_TIME_INTERVAL_MILLIS);
95            if (elapsedTimeCount <= 0) {
96                return;
97            }
98            if (elapsedTimeCount >= MAX_PUSH_ELAPSED) {
99                mLastTouchedTime = now;
100                mFc = 0;
101                return;
102            }
103            for (int i = 0; i < elapsedTimeCount; ++i) {
104                mLastTouchedTime += ELAPSED_TIME_INTERVAL_MILLIS;
105                mFc = pushElapsedTime(mFc);
106            }
107        }
108    }
109
110    /* package */ static  int fcToElapsedTime(byte fc) {
111        return fc & 0x0F;
112    }
113
114    /* package */ static int fcToCount(byte fc) {
115        return (fc >> 4) & 0x03;
116    }
117
118    /* package */ static int fcToLevel(byte fc) {
119        return (fc >> 6) & 0x03;
120    }
121
122    private static int calcFreq(int elapsedTime, int count, int level) {
123        if (level <= 0) {
124            // Reserved words, just return -1
125            return -1;
126        }
127        if (count == COUNT_MAX) {
128            // Temporary promote because it's frequently typed recently
129            ++level;
130        }
131        final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
132        final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
133        return MathUtils.SCORE_TABLE[l - 1][et];
134    }
135
136    /* pakcage */ static byte calcFc(int elapsedTime, int count, int level) {
137        final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
138        final int c = Math.min(COUNT_MAX, Math.max(0, count));
139        final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
140        return (byte)(et | (c << 4) | (l << 6));
141    }
142
143    public static int fcToFreq(byte fc) {
144        final int elapsedTime = fcToElapsedTime(fc);
145        final int count = fcToCount(fc);
146        final int level = fcToLevel(fc);
147        return calcFreq(elapsedTime, count, level);
148    }
149
150    public static byte pushElapsedTime(byte fc) {
151        int elapsedTime = fcToElapsedTime(fc);
152        int count = fcToCount(fc);
153        int level = fcToLevel(fc);
154        if (elapsedTime >= ELAPSED_TIME_MAX) {
155            // Downgrade level
156            elapsedTime = 0;
157            count = COUNT_MAX;
158            --level;
159        } else {
160            ++elapsedTime;
161        }
162        return calcFc(elapsedTime, count, level);
163    }
164
165    public static byte pushCount(byte fc, boolean isValid) {
166        final int elapsedTime = fcToElapsedTime(fc);
167        int count = fcToCount(fc);
168        int level = fcToLevel(fc);
169        if ((elapsedTime == 0 && count >= COUNT_MAX) || (isValid && level == 0)) {
170            // Upgrade level
171            ++level;
172            count = 0;
173            if (DEBUG) {
174                Log.d(TAG, "Upgrade level.");
175            }
176        } else {
177            ++count;
178        }
179        return calcFc(0, count, level);
180    }
181
182    // TODO: isValid should be false for a word whose frequency is 0,
183    // or that is not in the dictionary.
184    /**
185     * Check wheather we should save the bigram to the SQL DB or not
186     */
187    public static boolean needsToSave(byte fc, boolean isValid, boolean addLevel0Bigram) {
188        int level = fcToLevel(fc);
189        if (level == 0) {
190            if (isValid || !addLevel0Bigram) {
191                return false;
192            }
193        }
194        final int elapsedTime = fcToElapsedTime(fc);
195        return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0);
196    }
197
198    private static class MathUtils {
199        public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
200        static {
201            for (int i = 0; i < FC_LEVEL_MAX; ++i) {
202                final float initialFreq;
203                if (i >= 2) {
204                    initialFreq = FC_FREQ_MAX;
205                } else if (i == 1) {
206                    initialFreq = FC_FREQ_MAX / 2;
207                } else if (i == 0) {
208                    initialFreq = FC_FREQ_MAX / 4;
209                } else {
210                    continue;
211                }
212                for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
213                    final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
214                    final float freq = initialFreq
215                            * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS);
216                    final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
217                    SCORE_TABLE[i][j] = intFreq;
218                }
219            }
220        }
221    }
222}
223